File Coverage

lib/WWW/FleXtel.pm
Criterion Covered Total %
statement 24 181 13.2
branch 0 62 0.0
condition 0 31 0.0
subroutine 8 24 33.3
pod 6 8 75.0
total 38 306 12.4


line stmt bran cond sub pod time code
1             ############################################################
2             #
3             # $Id: FleXtel.pm 942 2007-02-06 18:51:21Z nicolaw $
4             # WWW::FleXtel - Manipulate FleXtel phone number redirection
5             #
6             # Copyright 2007 Nicola Worthington
7             #
8             # Licensed under the Apache License, Version 2.0 (the "License");
9             # you may not use this file except in compliance with the License.
10             # You may obtain a copy of the License at
11             #
12             # http://www.apache.org/licenses/LICENSE-2.0
13             #
14             # Unless required by applicable law or agreed to in writing, software
15             # distributed under the License is distributed on an "AS IS" BASIS,
16             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17             # See the License for the specific language governing permissions and
18             # limitations under the License.
19             #
20             ############################################################
21              
22             package WWW::FleXtel;
23             # vim:ts=4:sw=4:tw=78
24              
25 2     2   11087 use 5.6.1;
  2         9  
  2         98  
26 2     2   9 use strict;
  2         4  
  2         67  
27 2     2   9 use warnings;
  2         3  
  2         60  
28 2     2   2234 use LWP::UserAgent qw();
  2         99656  
  2         58  
29 2     2   21 use Scalar::Util qw(refaddr);
  2         5  
  2         211  
30 2     2   12 use Carp qw(croak cluck carp confess);
  2         2  
  2         115  
31 2     2   10 use vars qw($VERSION $DEBUG);
  2         4  
  2         5561  
32              
33             $VERSION = '0.03' || sprintf('%d', q$Revision: 942 $ =~ /(\d+)/g);
34             $DEBUG ||= $ENV{DEBUG} ? 1 : 0;
35              
36             my $objstore = {};
37              
38              
39             #
40             # Public methods
41             #
42              
43             sub new {
44 0 0   0 1   ref(my $class = shift) && croak 'Class name required';
45 0 0         croak 'Odd number of elements passed when even was expected' if @_ % 2;
46              
47             # Conjure up an invisible object
48 0           my $self = bless \(my $dummy), $class;
49 0           $objstore->{refaddr($self)} = {@_};
50 0           my $stor = $objstore->{refaddr($self)};
51              
52             # Define what parameters are valid for this constructor
53 0           $stor->{validkeys} = [qw(password account pin number timeout cache_ttl)];
54 0           my $validkeys = join('|',@{$stor->{validkeys}});
  0            
55              
56             # Only accept sensible known parameters from punters
57 0           my @invalidkeys = grep(!/^$validkeys$/,grep($_ ne 'validkeys',keys %{$stor}));
  0            
58 0           delete $stor->{$_} for @invalidkeys;
59 0 0 0       cluck('Unrecognised parameters passed: '.join(', ',@invalidkeys))
60             if @invalidkeys && $^W;
61              
62             # Set some default values
63 0 0 0       delete $stor->{timeout} if !defined $stor->{timeout} || $stor->{timeout} !~ /^[1-9]\d*$/;
64 0   0       $stor->{timeout} ||= 15; # 15 seconds
65 0 0 0       delete $stor->{cache_ttl} if !defined $stor->{cache_ttl} || $stor->{cache_ttl} !~ /^\d+$/;
66 0   0       $stor->{cache_ttl} ||= 5; # Cache data for 5 seconds
67 0   0       $stor->{'user-agent'} ||= sprintf('Mozilla/5.0 (X11; U; Linux i686; '.
68             'en-US; rv:1.8.1.1) Gecko/20060601 Firefox/2.0.0.1 (%s %s)',
69             __PACKAGE__, $VERSION);
70              
71             # Create LWP object
72 0           my $ua = new LWP::UserAgent;
73 0           $ua->env_proxy;
74 0           $ua->agent($stor->{'user-agent'});
75 0           $ua->timeout($stor->{timeout});
76 0           $ua->max_size(1024 * 200); # Hard code at 200KB
77 0           $stor->{ua} = $ua;
78              
79 0           DUMP('$self',$self);
80 0           DUMP('$stor',$stor);
81 0           return $self;
82             }
83              
84              
85 0     0 1   sub set_destination { &_executeQuery; }
86 0     0 1   sub get_destination { &_executeQuery; }
87 0     0 1   sub get_phonebook { &_executeQuery; }
88 0     0 1   sub get_email { &_executeQuery; }
89 0     0 1   sub get_icd { &_executeQuery; }
90              
91              
92              
93              
94             #
95             # Private methods
96             #
97              
98             sub _deepCopy{
99 0     0     my $this = shift;
100 0 0         if (!ref($this)) {
    0          
    0          
101 0           $this;
102             } elsif (ref($this) eq 'ARRAY') {
103 0           [ map _deepCopy($_), @{$this} ];
  0            
104             } elsif (ref($this) eq 'HASH'){
105 0           scalar { map { $_ => _deepCopy($this->{$_}) } keys %{$this} };
  0            
  0            
106             } else {
107 0           confess "What type is $_?";
108             }
109             }
110              
111              
112             sub _executeQuery {
113 0     0     my $self = shift;
114 0           local $Carp::CarpLevel = 1;
115 0 0 0       croak 'Not called as a method by parent object'
116             unless ref $self && UNIVERSAL::isa($self, __PACKAGE__);
117              
118             # Retrieve our object data stor and merge
119             # parameters from this method and the constructor
120 0           my $stor = $objstore->{refaddr($self)};
121 0           my %params = @_;
122 0           for my $k (@{$stor->{validkeys}}) {
  0            
123 0 0         $params{$k} = $stor->{$k}
124             unless defined $params{$k};
125             }
126              
127             # Figure out what method and personality we're running as
128 0           (my $subr = (caller(1))[3]) =~ s/.*:://;
129 0           my ($readCache,$cacheName) = split(/_/,$subr);
130 0 0         $readCache = 0 unless $readCache eq 'get';
131              
132             # Return from a cache if possible
133 0 0         if ($readCache) {
134 0           my $cache = $self->_readCache("$cacheName*$params{number}");
135 0 0         if (defined $cache) {
136 0           TRACE("Returning cache '$cacheName*$params{number}' ...");
137 0           return $cache;
138             }
139             }
140              
141             # Get the post query data required to send to the server
142 0           my ($mode,$query) = _getQueryData($subr);
143 0           TRACE("Running _executeQuery() in $mode mode ...");
144 0           DUMP('$query',$query);
145              
146             # Substitute keywords for data values
147 0           while (my ($k,$v) = each %{$query->{data}}) {
  0            
148 0           $query->{data}->{$k} =~ s/\@\@(\S+)\@\@/$params{$1}/g;
149             }
150              
151             # Post the request to the server
152 0           my $ua = $stor->{ua};
153 0           $ua->default_header('Referer' => $query->{referer});
154              
155 0           my $response;
156 0 0         if ($query->{method} eq 'GET') {
157 0           my $url = join('?',$query->{url}, join('&', map
158 0           { "$_=$query->{data}->{$_}" } keys %{$query->{data}}));
  0            
159 0           TRACE("GET $url");
160 0           $response = $ua->get($url);
161              
162             } else { # Default to POST
163 0           TRACE("POST $query->{url}");
164 0           $response = $ua->post($query->{url}, $query->{data});
165             };
166              
167             # Process and parse the HTML response if the request was successfull
168 0 0         if ($response->is_success) {
169 0           my $data = _extractData($response->content);
170 0           for my $cacheName (keys %{$data}) {
  0            
171 0 0         if (defined $data->{$cacheName}) {
172 0           $self->_writeCache("$cacheName*$params{number}", $data->{$cacheName});
173             }
174             }
175 0           return $data->{$cacheName};
176              
177             # Otherwise croak and die horribly
178             } else {
179 0           croak $response->status_line;
180             }
181             }
182              
183              
184             sub _readCache {
185 0     0     my ($self,$cacheName) = @_;
186 0           my $stor = $objstore->{refaddr($self)};
187              
188 0           TRACE("Checking age of cache '$cacheName' ...");
189 0 0 0       if (defined $stor->{cache}->{$cacheName}->{'last_updated'} &&
190             time - $stor->{cache}->{$cacheName}->{'last_updated'}
191             < $stor->{'cache_ttl'}) {
192 0           TRACE("Reading cache '$cacheName' ...");
193 0           return $stor->{cache}->{$cacheName}->{'data'};
194             }
195 0           return;
196             }
197              
198              
199             sub _writeCache {
200 0     0     my ($self,$cacheName,$ref) = @_;
201 0           my $stor = $objstore->{refaddr($self)};
202              
203 0           TRACE("Writing cache '$cacheName' ...");
204 0           $stor->{cache}->{$cacheName}->{'last_updated'} = time;
205 0           $stor->{cache}->{$cacheName}->{'data'} = $ref;
206             }
207              
208              
209             sub _phonebookLookup {
210 0     0     my ($phonebook, $lookup) = @_;
211 0 0         $lookup = '' unless defined $lookup;
212 0           my $memory = { destination => '', title => '', memory => '' };
213 0 0         return $memory unless $lookup =~ /\S/;
214              
215 0           for (my $i = 1; $i < @{$phonebook}; $i++) {
  0            
216 0           my $mem = $phonebook->[$i];
217 0           $mem->{memory} = $i;
218              
219 0 0 0       if ($lookup =~ /^\d+$/ && $i == $lookup) {
    0 0        
    0          
220 0           $memory = $mem;
221             } elsif ($lookup =~ /[0-9\#\+]{8,}/ && $lookup eq $mem->{number}) {
222 0           $memory = $mem;
223             } elsif ($lookup eq $mem->{title}) {
224 0           $memory = $mem;
225             }
226             }
227              
228 0           return $memory;
229             }
230              
231              
232             sub _extractData {
233 0     0     my $html = shift;
234 0           my %data;
235              
236 0 0         if ($html =~ /^\s*([0-9\#\+]{8,}(?:,.*)?)\s*$/s) {
237 0           my @args = split(/\s*,\s*/,$1);
238 0           s/(^\s*|\s*$)//gs for @args;
239 0           DUMP('@args',\@args);
240             # 01923000009,,01923111119,01992222221,01933333368,,,,,,nicolaw@lilacup.2x4b.com
241             # destination
242             # ICD destination
243             # memory 1, memory 2, memory 3, memory 4, memory 5, memory 6, memory 7, memory 8
244             # email address (flextel number specific - not account holder email)
245             # label 1, label 2, label 3, label 4, label 5, label 6, label 7, label 8
246              
247 0           $data{destination} = shift @args;
248 0 0         if (@args) {
249 0           $data{icd} = shift @args;
250 0           for (1..8) {
251 0           my $mem = shift @args;
252 0           $data{phonebook}->[$_]->{number} = $mem;
253 0           $data{phonebook}->[$_]->{memory} = $_;
254 0           TRACE("memory $_ => '$mem'");
255             }
256 0           $data{email} = shift @args;
257 0           for (1..8) {
258 0           my $title = shift @args;
259 0           $data{phonebook}->[$_]->{title} = $title;
260 0           $data{phonebook}->[$_]->{memory} = $_;
261 0           TRACE("memory title $_ => '$title'");
262             }
263             }
264 0           return \%data;
265             }
266              
267             # Nasty Javascript scraping
268 0           for (split(/[\n\r]/,$html)) {
269 0           chomp;
270 0 0         if (my ($key,$num,$val) = $_ =~
271             /^\s*FN.(email|dest_(?:no|nrb)|mem(\d+)(?:text)?)\s*=\s*(.+?)\s*;\s*$/) {
272 0           $val =~ s/^\s*"\s*//g;
273 0           $val =~ s/\s*"\s*$//g;
274              
275 0 0 0       if (my ($index) = $key =~ /^mem(\d+)$/) {
    0          
    0          
    0          
276 0           $val =~ s/[^0-9\#]//g;
277 0           $data{phonebook}->[$num]->{number} = $val;
278 0           $data{phonebook}->[$num]->{memory} = $index;
279              
280             } elsif ($key =~ /^mem(\d+)text$/) {
281 0           $data{phonebook}->[$num]->{title} = $val;
282 0           $data{phonebook}->[$num]->{memory} = $1;
283              
284             } elsif ($key eq 'email' && $val =~ /"(\S+?)"/) {
285 0           $data{email} = $1;
286              
287             } elsif ($key =~ /^dest_no$/) {
288 0           ($data{destination}) = $val =~ /([0-9\#\+]{8,})/;
289 0           $data{destination} =~ s/[^0-9\#]//g;
290             }
291             }
292             }
293              
294 0           return \%data;
295             }
296              
297              
298             sub _getQueryData {
299 0     0     my $subr = shift;
300              
301 0           my %subrMap = (
302             'set_destination' => 'divert_simple',
303             'get_destination' => 'getpin_simple',
304             'get_phonebook' => 'getpin_simple',
305             'get_email' => 'getpin_simple',
306             'get_icd' => 'getpin_simple',
307             );
308              
309 0           my %queries = (
310             'account_post' => {
311             'method' => 'POST',
312             'url' => 'https://www.flextel.ltd.uk/cgi-bin/account.sh',
313             'referer' => 'Referer=https://www.flextel.ltd.uk/cgi-bin/passthru.sh?f=account&h=logon',
314             'data' => {
315             'mode' => 'logon',
316             'cust_id' => '@@account@@',
317             'flextel' => '',
318             'start' => '1',
319             'total' => '9999',
320             'control' => '',
321             'acc_no' => '@@account@@',
322             'pwd' => '@@password@@',
323             'Logon' => 'Logon',
324             },
325             },
326             'getpin_simple' => {
327             'method' => 'GET',
328             'url' => 'https://www.flextel.ltd.uk/cgi-bin/reroute.sh',
329             'referer' => '',
330             'data' => {
331             'mode' => 'getpin',
332             'flextel' => '@@number@@',
333             'pin' => '@@pin@@',
334             'alt' => 'simple',
335             },
336             },
337             'getpin_post' => {
338             'method' => 'POST',
339             'url' => 'https://www.flextel.ltd.uk/cgi-bin/reroute.sh',
340             'referer' => 'https://www.flextel.ltd.uk/cgi-bin/reroute.sh?flextel=',
341             'data' => {
342             'mode' => 'getpin',
343             'flextel' => '@@number@@',
344             'cust_id' => '',
345             'pwd' => '',
346             'flexnum' => '@@number@@',
347             'pin' => '@@pin@@',
348             'Logon' => 'Logon',
349             },
350             },
351             'divert_simple' => {
352             'method' => 'GET',
353             'url' => 'https://www.flextel.ltd.uk/cgi-bin/reroute.sh',
354             'referer' => '',
355             'data' => {
356             'mode' => 'divert',
357             'flextel' => '@@number@@',
358             'pin' => '@@pin@@',
359             'new_dest' => '@@destination@@',
360             'dest_nrb' => '',
361             'alt' => 'simple',
362             },
363             },
364             'divert_post' => {
365             'method' => 'POST',
366             'url' => 'https://www.flextel.ltd.uk/cgi-bin/reroute.sh',
367             'referer' => 'https://www.flextel.ltd.uk/cgi-bin/reroute.sh',
368             'data' => {
369             'f' => '',
370             'h' => '',
371             'alt' => '',
372             'source' => '',
373             'mode' => 'divert',
374             'flextel' => '@@number@@',
375             'pin' => '@@pin@@',
376             'pwd' => '',
377             'new_dest' => '@@destination@@',
378             'dest_nrb' => '',
379             'nba' => '3Ba',
380             'start' => '',
381             'present' => 'false',
382             'mask' => 'false',
383             'SelectDest' => '@@destination@@',
384             'SelectNRB' => 'null',
385             'checkboxBusy' => 'checkbox',
386             'selectTimeoutNR' => '3',
387             },
388             },
389             );
390              
391 0           my $mode = $subrMap{$subr};
392 0           return ($mode, _deepCopy($queries{$mode}));
393             }
394              
395              
396             sub DESTROY {
397 0     0     my $self = shift;
398 0           delete $objstore->{refaddr($self)};
399             }
400              
401              
402             sub TRACE {
403 0 0   0 0   return unless $DEBUG;
404 0           carp(shift());
405             }
406              
407              
408             sub DUMP {
409 0 0   0 0   return unless $DEBUG;
410 0           eval {
411 0           require Data::Dumper;
412 2     2   15 no warnings 'once';
  2         3  
  2         271  
413 0           local $Data::Dumper::Indent = 2;
414 0           local $Data::Dumper::Terse = 1;
415 0           carp(shift().': '.Data::Dumper::Dumper(shift()));
416             }
417             }
418              
419              
420             1;
421              
422              
423              
424             =pod
425              
426             =head1 NAME
427              
428             WWW::FleXtel - Manipulate FleXtel phone number redirection
429              
430             =head1 SYNOPSIS
431              
432             use strict;
433             use WWW::FleXtel qw();
434             use Data::Dumper qw(Dumper);
435            
436             my $flextel = WWW::FleXtel->new(
437             number => "0701776655",
438             pin => "1234",
439             account => "A99999", # not required
440             password => "password", # not required
441             );
442            
443             printf("Diverted to %s\n", $flextel->get_destination);
444             printf("Diverted to %s\n", $flextel->set_destination(
445             destination => "01923001122"
446             );
447            
448             print Dumper($flextel->get_phonebook);
449              
450             =head1 DESCRIPTION
451              
452             This module provides a very basic OO interface to FleXtel telephone
453             number redirection webpage.
454              
455             =head1 METHODS
456              
457             =head2 new
458              
459             my $flextel = WWW::FleXtel->new(
460             number => "0701776655",
461             pin => "1234",
462             account => "A99999", # not required
463             password => "password", # not required
464             );
465              
466             Create a new WWW::FleXtel object. Currently the I and
467             I parameters are unsed and therefor do not need to be passed
468             to this constructor method.
469              
470             This method does have any mandatory parameters. However values passed
471             this constructor method will be used as default fallback values if they
472             are not passed to the subsequent accessor methods detailed below.
473              
474             =over 4
475              
476             =item number
477              
478             Specifies the default FleXtel number to use for all subsequent queries.
479              
480             =item pin
481              
482             Specifies the default PIN to use for all subsqeuent queries.
483              
484             =item account
485              
486             Specifies the default FleXtel account number to use for all subsequent
487             queries. This parameter is not currently used, but may be used in future
488             releases.
489              
490             =item password
491              
492             Specifies the default account password to use for all subsequent queries.
493             This parameter is not currently use, but may be used in future releases.
494              
495             =item timeout
496              
497             Specifies (in seconds) the timeout for all HTTP connections. By default
498             this is set to 15 seconds.
499              
500             =item cache_ttl
501              
502             Specifies (in seconds) the TTL for values to be cached internally within
503             the WWW::FleXtel object. By default this is set to 5 seconds.
504              
505             =back
506              
507             =head2 get_destination
508              
509             my $destination = $flextel->get_destination;
510             print "Diverted to $destination\n";
511              
512             Retrieves the destination telephone number that your FleXtel number is
513             currently diverted to.
514              
515             =head2 set_destination
516              
517             my $destination = $flextel->set_destination(destination => "01923001122");
518             print "Diverted to $destination\n";
519              
520             Sets the destination telephone number that your FleXtel number is
521             diverted to.
522              
523             =head2 get_phonebook
524              
525             my $phonebook = $flextel->get_phonebook;
526             use Data::Dumper qw(Dumper);
527             print Dumper($phonebook);
528            
529             my $destination = $flextel->get_destination;
530             my ($person) = grep(/\S/, map {
531             $_->{title} if defined $_ && $_->{number} eq $destination
532             } @{$phonebook}); $person ||= "*not recorded*";
533             print "$destination is $person in your phonebook\n";
534              
535             This method extracts the indexes, names and numbers from your FleXtel
536             number's phonebook.
537              
538             =head2 get_icd
539              
540             my $icd = $flextel->get_icd;
541              
542             =head2 get_email
543              
544             my $notification_address = $flextel->get_email;
545              
546             =head1 TODO
547              
548             Add support for retrieving a list of all FleXtel phone numbers
549             attached to an account number.
550              
551             =head1 SEE ALSO
552              
553             L
554              
555             =head1 VERSION
556              
557             $Id: FleXtel.pm 942 2007-02-06 18:51:21Z nicolaw $
558              
559             =head1 AUTHOR
560              
561             Nicola Worthington
562              
563             L
564              
565             If you like this software, why not show your appreciation by sending the
566             author something nice from her
567             L?
568             ( http://www.amazon.co.uk/gp/registry/1VZXC59ESWYK0?sort=priority )
569              
570             =head1 ACKNOWLEDGEMENTS
571              
572             Special thanks to Kevin Archer at FleXtel and the FleXtel support and
573             development team for implementing the simple CVS access methods to their
574             website.
575              
576             See CREDITS in the distribution tarball.
577              
578             =head1 COPYRIGHT
579              
580             Copyright 2007 Nicola Worthington.
581              
582             This software is licensed under The Apache Software License, Version 2.0.
583              
584             L
585              
586             =cut
587              
588              
589             __END__