| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package WWW::FBX; | 
| 2 | 23 |  |  | 23 |  | 301800 | use Moose; | 
|  | 23 |  |  |  |  | 6783503 |  | 
|  | 23 |  |  |  |  | 144 |  | 
| 3 | 23 |  |  | 23 |  | 118491 | use Carp::Clan qw/^(?:WWW::FBX|Moose|Class::MOP)/; | 
|  | 23 |  |  |  |  | 30757 |  | 
|  | 23 |  |  |  |  | 143 |  | 
| 4 | 23 |  |  | 23 |  | 12003 | use JSON::MaybeXS; | 
|  | 23 |  |  |  |  | 98669 |  | 
|  | 23 |  |  |  |  | 1112 |  | 
| 5 | 23 |  |  | 23 |  | 106 | use Scalar::Util qw/reftype/; | 
|  | 23 |  |  |  |  | 29 |  | 
|  | 23 |  |  |  |  | 841 |  | 
| 6 | 23 |  |  | 23 |  | 8515 | use URI::Escape; | 
|  | 23 |  |  |  |  | 22886 |  | 
|  | 23 |  |  |  |  | 1074 |  | 
| 7 | 23 |  |  | 23 |  | 9206 | use HTTP::Request::Common; | 
|  | 23 |  |  |  |  | 346435 |  | 
|  | 23 |  |  |  |  | 1265 |  | 
| 8 | 23 |  |  | 23 |  | 8407 | use WWW::FBX::Error; | 
|  | 23 |  |  |  |  | 64 |  | 
|  | 23 |  |  |  |  | 991 |  | 
| 9 | 23 |  |  | 23 |  | 13636 | use Encode qw/encode_utf8/; | 
|  | 23 |  |  |  |  | 166898 |  | 
|  | 23 |  |  |  |  | 1541 |  | 
| 10 | 23 |  |  | 23 |  | 116 | use Try::Tiny; | 
|  | 23 |  |  |  |  | 32 |  | 
|  | 23 |  |  |  |  | 879 |  | 
| 11 | 23 |  |  | 23 |  | 12158 | use LWP::UserAgent; | 
|  | 23 |  |  |  |  | 357987 |  | 
|  | 23 |  |  |  |  | 997 |  | 
| 12 |  |  |  |  |  |  |  | 
| 13 |  |  |  |  |  |  | with 'WWW::FBX::Role::API::APIv3'; | 
| 14 |  |  |  |  |  |  | with 'WWW::FBX::Role::Auth'; | 
| 15 |  |  |  |  |  |  |  | 
| 16 | 23 |  |  | 23 |  | 152 | use namespace::autoclean; | 
|  | 23 |  |  |  |  | 31 |  | 
|  | 23 |  |  |  |  | 177 |  | 
| 17 |  |  |  |  |  |  |  | 
| 18 |  |  |  |  |  |  | our $VERSION = "0.18"; | 
| 19 |  |  |  |  |  |  |  | 
| 20 |  |  |  |  |  |  | has base_url    => ( isa => 'Str', is => 'ro', default => 'http://mafreebox.free.fr' ); | 
| 21 |  |  |  |  |  |  | has lwp_args    => ( isa => 'HashRef', is => 'ro', default => sub { {} } ); | 
| 22 |  |  |  |  |  |  | has [ qw/app_id app_name app_version device_name/ ] => ( | 
| 23 |  |  |  |  |  |  | isa => 'Str', is => 'ro', required => 1 ); | 
| 24 |  |  |  |  |  |  | has ua          => ( isa => 'LWP::UserAgent', is => 'rw', lazy => 1, builder => '_build_ua' ); | 
| 25 |  |  |  |  |  |  | has uar         => ( isa => 'HashRef', is => 'rw' ); | 
| 26 |  |  |  |  |  |  | has uarh        => ( isa => 'HTTP::Response', is => 'rw' ); | 
| 27 |  |  |  |  |  |  | has debug       => ( isa => 'Bool', is => 'rw', default => 0, trigger => \&_set_debug ); | 
| 28 |  |  |  |  |  |  |  | 
| 29 |  |  |  |  |  |  | has _json_handler   => ( | 
| 30 |  |  |  |  |  |  | is      => 'rw', | 
| 31 |  |  |  |  |  |  | default => sub { JSON->new->allow_nonref }, | 
| 32 |  |  |  |  |  |  | handles => { from_json => 'decode' }, | 
| 33 |  |  |  |  |  |  | ); | 
| 34 |  |  |  |  |  |  |  | 
| 35 |  |  |  |  |  |  | sub _set_debug { | 
| 36 | 0 |  |  | 0 |  |  | my ( $self, $debug, $odebug) = @_ ; | 
| 37 | 0 | 0 | 0 |  |  |  | if ( defined $odebug and $odebug != $debug or $debug ) { | 
|  |  |  | 0 |  |  |  |  | 
| 38 | 0 | 0 |  |  |  |  | if ($debug) { | 
| 39 | 0 |  |  | 0 |  |  | $self->ua->add_handler("request_send", sub { print ">" x 25, "\n"; shift->dump; return }); | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 40 | 0 |  |  | 0 |  |  | $self->ua->add_handler("response_done", sub { print "<" x 25, "\n"; shift->dump; return }); | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 41 |  |  |  |  |  |  | } else { | 
| 42 | 0 |  |  |  |  |  | $self->ua->remove_handler("request_send"); | 
| 43 | 0 |  |  |  |  |  | $self->ua->remove_handler("response_done"); | 
| 44 |  |  |  |  |  |  | } | 
| 45 |  |  |  |  |  |  | } | 
| 46 |  |  |  |  |  |  | } | 
| 47 |  |  |  |  |  |  |  | 
| 48 |  |  |  |  |  |  | sub _build_ua { | 
| 49 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 50 |  |  |  |  |  |  |  | 
| 51 | 0 |  |  |  |  |  | my $ua = LWP::UserAgent->new(%{$self->lwp_args}); | 
|  | 0 |  |  |  |  |  |  | 
| 52 |  |  |  |  |  |  |  | 
| 53 | 0 |  |  |  |  |  | return $ua; | 
| 54 |  |  |  |  |  |  | } | 
| 55 |  |  |  |  |  |  |  | 
| 56 |  |  |  |  |  |  | sub _json_request { | 
| 57 | 0 |  |  | 0 |  |  | my ($self, $http_method, $uri, $args, $content_type ) = @_; | 
| 58 |  |  |  |  |  |  |  | 
| 59 | 0 |  |  |  |  |  | my $msg = $self->_prepare_request($http_method, $uri, $args, $content_type); | 
| 60 | 0 |  |  |  |  |  | my $res = $self->_send_request($msg); | 
| 61 |  |  |  |  |  |  |  | 
| 62 |  |  |  |  |  |  | #Store response content | 
| 63 | 0 |  |  |  |  |  | $self->uar( $self->_parse_result ($res, $args ) ); | 
| 64 |  |  |  |  |  |  |  | 
| 65 |  |  |  |  |  |  | #And HTTP response RAW | 
| 66 | 0 |  |  |  |  |  | $self->uarh( $res ); | 
| 67 |  |  |  |  |  |  |  | 
| 68 | 0 |  |  |  |  |  | return $self->uar->{result}; | 
| 69 |  |  |  |  |  |  | } | 
| 70 |  |  |  |  |  |  |  | 
| 71 |  |  |  |  |  |  | sub _prepare_request { | 
| 72 | 0 |  |  | 0 |  |  | my ($self, $http_method, $uri, $args, $content_type ) = @_; | 
| 73 |  |  |  |  |  |  |  | 
| 74 | 0 |  |  |  |  |  | my $msg; | 
| 75 |  |  |  |  |  |  |  | 
| 76 | 0 | 0 |  |  |  |  | if( $http_method eq 'PUT' ) { | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 77 | 0 |  |  |  |  |  | $msg = PUT( $uri, Content => encode_json  $args  ); | 
| 78 |  |  |  |  |  |  | } | 
| 79 |  |  |  |  |  |  | elsif ( $http_method =~ /^(?:GET|DELETE)$/ ) { | 
| 80 | 0 | 0 |  |  |  |  | $uri->query($self->_query_string_for($args)) if keys %$args; | 
| 81 | 0 |  |  |  |  |  | $msg = HTTP::Request->new($http_method, $uri); | 
| 82 |  |  |  |  |  |  | } | 
| 83 |  |  |  |  |  |  | elsif ( $http_method eq 'POST' ) { | 
| 84 | 0 | 0 | 0 |  |  |  | if( !$content_type or $content_type eq 'application/json' ) { | 
|  |  | 0 |  |  |  |  |  | 
| 85 | 0 |  |  |  |  |  | $msg = POST( $uri,  Content_Type => 'application/json', Content =>  encode_json $args ); | 
| 86 |  |  |  |  |  |  | } | 
| 87 |  |  |  |  |  |  | elsif ( $content_type eq "form-data" ) { | 
| 88 | 0 | 0 |  |  |  |  | $msg = POST($uri, Content_Type => 'form-data', Content => [ map { ref $_ ? $_ : encode_utf8 $_ } %$args ]); | 
|  | 0 |  |  |  |  |  |  | 
| 89 |  |  |  |  |  |  | } | 
| 90 |  |  |  |  |  |  | else { | 
| 91 | 0 |  |  |  |  |  | $msg = POST($uri, Content => $args); | 
| 92 |  |  |  |  |  |  | } | 
| 93 |  |  |  |  |  |  | } | 
| 94 |  |  |  |  |  |  | else { | 
| 95 | 0 |  |  |  |  |  | croak "unexpected HTTP method: $http_method"; | 
| 96 |  |  |  |  |  |  | } | 
| 97 |  |  |  |  |  |  |  | 
| 98 | 0 |  |  |  |  |  | return $msg; | 
| 99 |  |  |  |  |  |  | } | 
| 100 |  |  |  |  |  |  |  | 
| 101 |  |  |  |  |  |  | sub _query_string_for { | 
| 102 | 0 |  |  | 0 |  |  | my ( $self, $args ) = @_; | 
| 103 |  |  |  |  |  |  |  | 
| 104 | 0 |  |  |  |  |  | my @pairs; | 
| 105 | 0 |  |  |  |  |  | while ( my ($k, $v) = each %$args ) { | 
| 106 | 0 |  |  |  |  |  | push @pairs, join '=', $k, $v; | 
| 107 |  |  |  |  |  |  | } | 
| 108 |  |  |  |  |  |  |  | 
| 109 | 0 |  |  |  |  |  | return join '&', @pairs; | 
| 110 |  |  |  |  |  |  | } | 
| 111 |  |  |  |  |  |  |  | 
| 112 | 0 |  |  | 0 |  |  | sub _send_request { shift->ua->request(shift) } | 
| 113 |  |  |  |  |  |  |  | 
| 114 |  |  |  |  |  |  | sub _parse_result { | 
| 115 | 0 |  |  | 0 |  |  | my ($self, $res, $args) = @_; | 
| 116 |  |  |  |  |  |  |  | 
| 117 | 0 |  |  |  |  |  | my $content = $res->content; | 
| 118 |  |  |  |  |  |  |  | 
| 119 | 0 | 0 |  | 0 |  |  | my $j_obj = length $content ? try { $self->from_json($content) } : {}; | 
|  | 0 |  |  |  |  |  |  | 
| 120 |  |  |  |  |  |  |  | 
| 121 |  |  |  |  |  |  | #Die if message contains an API error (even on HTTP 200) | 
| 122 | 0 | 0 | 0 |  |  |  | if ( ref $j_obj && reftype $j_obj eq 'HASH' && (exists $j_obj->{error_code} || exists $j_obj->{msg} ) ) { | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
| 123 | 0 |  |  |  |  |  | die WWW::FBX::Error->new(fbx_error => $j_obj, http_response => $res); | 
| 124 |  |  |  |  |  |  | } | 
| 125 |  |  |  |  |  |  |  | 
| 126 |  |  |  |  |  |  | #If no API error and HTTP is 200 and answer is json | 
| 127 | 0 | 0 | 0 |  |  |  | return $j_obj if $res->is_success && defined $j_obj; | 
| 128 |  |  |  |  |  |  |  | 
| 129 |  |  |  |  |  |  | #API Download file does not return JSON!! | 
| 130 |  |  |  |  |  |  | #If answer is 200 and not json, return unchanged (but still pack it in an HashRef for uar type check..) | 
| 131 | 0 | 0 | 0 |  |  |  | return { result => { filename => $res->filename, content => $content } } if $res->filename and $res->is_success; | 
| 132 |  |  |  |  |  |  |  | 
| 133 |  |  |  |  |  |  | #Else die on HTTP failures, which might contain a json response or not | 
| 134 | 0 |  |  |  |  |  | my $error = WWW::FBX::Error->new(http_response => $res); | 
| 135 | 0 | 0 |  |  |  |  | $error->fbx_error($j_obj) if ref $j_obj; | 
| 136 |  |  |  |  |  |  |  | 
| 137 | 0 |  |  |  |  |  | die $error; | 
| 138 |  |  |  |  |  |  | } | 
| 139 |  |  |  |  |  |  |  | 
| 140 |  |  |  |  |  |  | __PACKAGE__->meta->make_immutable; | 
| 141 |  |  |  |  |  |  |  | 
| 142 |  |  |  |  |  |  | 1; | 
| 143 |  |  |  |  |  |  | __END__ | 
| 144 |  |  |  |  |  |  |  | 
| 145 |  |  |  |  |  |  | =for html <a href="https://travis-ci.org/architek/WWW-FBX"><img src="https://travis-ci.org/architek/WWW-FBX.svg?branch=master"></a> | 
| 146 |  |  |  |  |  |  |  | 
| 147 |  |  |  |  |  |  | =encoding utf-8 | 
| 148 |  |  |  |  |  |  |  | 
| 149 |  |  |  |  |  |  | =head1 NAME | 
| 150 |  |  |  |  |  |  |  | 
| 151 |  |  |  |  |  |  | WWW::FBX - Freebox v6 OS Perl Interface | 
| 152 |  |  |  |  |  |  |  | 
| 153 |  |  |  |  |  |  | =head1 FREEBOX SDK API 3.0 | 
| 154 |  |  |  |  |  |  |  | 
| 155 |  |  |  |  |  |  | This version provides the API 3.0 support through the APIv3 role but other version can be provided by creating a new role. | 
| 156 |  |  |  |  |  |  |  | 
| 157 |  |  |  |  |  |  | =head1 AUTHENTICATION | 
| 158 |  |  |  |  |  |  |  | 
| 159 |  |  |  |  |  |  | Authentication is provided through the Auth role but other authentication mechanism can be provided by creating a new role. | 
| 160 |  |  |  |  |  |  |  | 
| 161 |  |  |  |  |  |  | =head1 SYNOPSIS | 
| 162 |  |  |  |  |  |  |  | 
| 163 |  |  |  |  |  |  | use WWW::FBX; | 
| 164 |  |  |  |  |  |  | use Scalar::Util 'blessed'; | 
| 165 |  |  |  |  |  |  |  | 
| 166 |  |  |  |  |  |  | my $res; | 
| 167 |  |  |  |  |  |  | eval { | 
| 168 |  |  |  |  |  |  | my $fbx = WWW::FBX->new( | 
| 169 |  |  |  |  |  |  | app_id => "APP ID", | 
| 170 |  |  |  |  |  |  | app_name => "APP NAME", | 
| 171 |  |  |  |  |  |  | app_version => "1.0", | 
| 172 |  |  |  |  |  |  | device_name => "MY DEVICE", | 
| 173 |  |  |  |  |  |  | track_id => "48", | 
| 174 |  |  |  |  |  |  | app_token => "2/g43EZYD8AO7tbnwwhmMxMuELtTCyQrV1goMgaepHWGrqWlloWmMRszCuiN2ftp", | 
| 175 |  |  |  |  |  |  | base_url => "http://12.34.56.78:3333", | 
| 176 |  |  |  |  |  |  | debug => 1, | 
| 177 |  |  |  |  |  |  | ); | 
| 178 |  |  |  |  |  |  | print "You are now authenticated with track_id ", $fbx->track_id, " and app_token ", $fbx->app_token, "\n"; | 
| 179 |  |  |  |  |  |  | print "App permissions are:\n"; | 
| 180 |  |  |  |  |  |  | while ( my( $key, $value ) = each %{ $fbx->uar->{result}{permissions} } ) { | 
| 181 |  |  |  |  |  |  | print "\t $key\n" if $value; | 
| 182 |  |  |  |  |  |  | } | 
| 183 |  |  |  |  |  |  |  | 
| 184 |  |  |  |  |  |  | $res = $fbx->connection; | 
| 185 |  |  |  |  |  |  | print "Your ", $res->{media}, " internet connection state is ", $res->{state}, "\n"; | 
| 186 |  |  |  |  |  |  | $fbx->set_ftp_config( {enabled => \1} ); | 
| 187 |  |  |  |  |  |  | $fbx->reset_freeplug( "F4:CA:E5:DE:AD:BE/reset" ); | 
| 188 |  |  |  |  |  |  | }; | 
| 189 |  |  |  |  |  |  |  | 
| 190 |  |  |  |  |  |  | if ( my $err = $@ ) { | 
| 191 |  |  |  |  |  |  | die $@ unless blessed $err && $err->isa('WWW::FBX::Error'); | 
| 192 |  |  |  |  |  |  |  | 
| 193 |  |  |  |  |  |  | warn "HTTP Response Code: ", $err->code, "\n", | 
| 194 |  |  |  |  |  |  | "HTTP Message......: ", $err->message, "\n", | 
| 195 |  |  |  |  |  |  | "API Error.........: ", $err->error, "\n", | 
| 196 |  |  |  |  |  |  | "Error Code........: ", $err->fbx_error_code, "\n", | 
| 197 |  |  |  |  |  |  | } | 
| 198 |  |  |  |  |  |  |  | 
| 199 |  |  |  |  |  |  | =head1 DESCRIPTION | 
| 200 |  |  |  |  |  |  |  | 
| 201 |  |  |  |  |  |  | This module provides a perl interface to the L<Freebox|https://en.wikipedia.org/wiki/Freebox#V6_generation.2C_Freebox_Revolution> v6 APIs. | 
| 202 |  |  |  |  |  |  |  | 
| 203 |  |  |  |  |  |  | See L<http://dev.freebox.fr/sdk/os/> for a full description of the APIs. | 
| 204 |  |  |  |  |  |  |  | 
| 205 |  |  |  |  |  |  | =head1 METHODS AND ARGUMENTS | 
| 206 |  |  |  |  |  |  |  | 
| 207 |  |  |  |  |  |  | my $fbx = WWW::FBX->new( app_id => "APP ID", app_name => "APP NAME", | 
| 208 |  |  |  |  |  |  | app_version => "1.0", device_name => "device" ); | 
| 209 |  |  |  |  |  |  |  | 
| 210 |  |  |  |  |  |  | my $fbx = WWW::FBX->new( app_id => "APP ID", app_name => "APP NAME", | 
| 211 |  |  |  |  |  |  | app_version => "1.0", device_name => "device", | 
| 212 |  |  |  |  |  |  | track_id => "48", app_token => "2/g43EZYD8AO7tbnwwhmMxMuELtTCyQrV1goMgaepHWGrqWlloWmMRszCuiN2ftp", | 
| 213 |  |  |  |  |  |  | base_url => "http://12.34.56.78:3333" , | 
| 214 |  |  |  |  |  |  | debug => 1 ); | 
| 215 |  |  |  |  |  |  |  | 
| 216 |  |  |  |  |  |  | Mandatory constructor parameters are app_id, app_name, app_version, device_name. | 
| 217 |  |  |  |  |  |  | When track_id and app_token are also provided, they will be used to authenticate. | 
| 218 |  |  |  |  |  |  | Otherwise, new track_id and app_token will be given by the freebox. These can be then used for later access. | 
| 219 |  |  |  |  |  |  | base_url defaults to http://mafreebox.free.fr which is the base uri when accessing the freebox from the LAN side. | 
| 220 |  |  |  |  |  |  |  | 
| 221 |  |  |  |  |  |  | Note that adding the I<settings> or I<parental> permissions is only possible through the web interface (Paramètres de la Freebox -> Gestion des accès -> Applications) | 
| 222 |  |  |  |  |  |  |  | 
| 223 |  |  |  |  |  |  | The constructor takes care of detecting the API version and authentication. | 
| 224 |  |  |  |  |  |  |  | 
| 225 |  |  |  |  |  |  |  | 
| 226 |  |  |  |  |  |  | The return value of all api methods is the L<result|http://dev.freebox.fr/sdk/os/#APIResponse.result> structure of APIResponse, or undef if no result is returned. | 
| 227 |  |  |  |  |  |  |  | 
| 228 |  |  |  |  |  |  | The full json response of the last request is available through the uar method (usefull when using the I<new> method) and the complete HTTP::Response is available through the uarh method. | 
| 229 |  |  |  |  |  |  |  | 
| 230 |  |  |  |  |  |  | Api methods will I<die> if the APIResponse is an error. It is up to the caller to handle this exception. | 
| 231 |  |  |  |  |  |  |  | 
| 232 |  |  |  |  |  |  | =head1 QUICK START | 
| 233 |  |  |  |  |  |  |  | 
| 234 |  |  |  |  |  |  | The list of currently available services implemented in this module is given in L<WWW::FBX::Role::API::APIv3>. | 
| 235 |  |  |  |  |  |  |  | 
| 236 |  |  |  |  |  |  | A script called fbx_test.pl is provided in script to show how to send commands and handle exceptions. | 
| 237 |  |  |  |  |  |  |  | 
| 238 |  |  |  |  |  |  | It can also be used standalone to get tokens and send a command. | 
| 239 |  |  |  |  |  |  |  | 
| 240 |  |  |  |  |  |  | On first call, you will be requested to physically authenticate on the freebox itself. Once done, the token is stored in the current directory in a file called app_token. You can then grant all permissions on the freebox web interface to allow all commands. | 
| 241 |  |  |  |  |  |  |  | 
| 242 |  |  |  |  |  |  | When suffix parameter is required, pass it as a normal parameter. | 
| 243 |  |  |  |  |  |  |  | 
| 244 |  |  |  |  |  |  | When more parameters are required, it is possible to send a json structure, see EXAMPLES. You need to escape the accolades though. | 
| 245 |  |  |  |  |  |  |  | 
| 246 |  |  |  |  |  |  | =head1 EXAMPLES | 
| 247 |  |  |  |  |  |  |  | 
| 248 |  |  |  |  |  |  | fbx-test.pl --help | 
| 249 |  |  |  |  |  |  | fbx-test.pl connection | 
| 250 |  |  |  |  |  |  | fbx-test.pl system | 
| 251 |  |  |  |  |  |  | fbx-test.pl call_log | 
| 252 |  |  |  |  |  |  | fbx-test.pl call_log 2053 | 
| 253 |  |  |  |  |  |  | fbx-test.pl reboot | 
| 254 |  |  |  |  |  |  | fbx-test.pl reset_freeplug F4:CA:42:22:53:EF/reset | 
| 255 |  |  |  |  |  |  | fbx-test.pl cp '{"files":["Disque dur/ds.txt"], "dst":"Disque dur/Temp", "mode":"both"}' | 
| 256 |  |  |  |  |  |  |  | 
| 257 |  |  |  |  |  |  | =head1 LICENSE | 
| 258 |  |  |  |  |  |  |  | 
| 259 |  |  |  |  |  |  | Copyright (C) Laurent Kislaire. | 
| 260 |  |  |  |  |  |  |  | 
| 261 |  |  |  |  |  |  | This library is free software; you can redistribute it and/or modify | 
| 262 |  |  |  |  |  |  | it under the same terms as Perl itself. | 
| 263 |  |  |  |  |  |  |  | 
| 264 |  |  |  |  |  |  | =head1 AUTHOR | 
| 265 |  |  |  |  |  |  |  | 
| 266 |  |  |  |  |  |  | Laurent Kislaire E<lt>teebeenator@gmail.comE<gt> | 
| 267 |  |  |  |  |  |  |  | 
| 268 |  |  |  |  |  |  | =cut | 
| 269 |  |  |  |  |  |  |  |