File Coverage

blib/lib/Pootle/Agent.pm
Criterion Covered Total %
statement 37 39 94.8
branch n/a
condition n/a
subroutine 13 13 100.0
pod n/a
total 50 52 96.1


line stmt bran cond sub pod time code
1             # Copyright (C) 2017 Koha-Suomi
2             #
3             # This file is part of Pootle-Client.
4              
5             package Pootle::Agent;
6              
7 5     5   31 use Modern::Perl '2015';
  5         10  
  5         45  
8 5     5   633 use utf8;
  5         11  
  5         23  
9             binmode STDOUT, ':encoding(UTF-8)';
10             binmode STDERR, ':encoding(UTF-8)';
11 5     5   201 use feature 'signatures'; no warnings "experimental::signatures";
  5     5   11  
  5         133  
  5         26  
  5         8  
  5         165  
12 5     5   26 use Carp::Always;
  5         8  
  5         92  
13 5     5   33 use Try::Tiny;
  5         9  
  5         230  
14 5     5   28 use Scalar::Util qw(blessed);
  5         9  
  5         221  
15              
16             =head2 Pootle::Agent
17              
18             LWP::Curl wrapper to deal with various types of exceptions transparently
19              
20             =cut
21              
22 5     5   28 use Params::Validate qw(:all);
  5         10  
  5         625  
23 5     5   2186 use LWP::UserAgent;
  5         186036  
  5         241  
24 5     5   2094 use Encode;
  5         40585  
  5         415  
25 5     5   1576 use MIME::Base64;
  5         2894  
  5         320  
26 5     5   2510 use JSON::XS;
  5         19011  
  5         270  
27 5     5   409 use File::Slurp;
  0            
  0            
28              
29             use Pootle::Logger;
30             my $l = bless({}, 'Pootle::Logger'); #Lazy load package logger this way to avoid circular dependency issues with logger includes from many packages
31              
32             use Pootle::Exception;
33             use Pootle::Exception::HTTP::MethodNotAllowed;
34             use Pootle::Exception::HTTP::NotFound;
35             use Pootle::Exception::Credentials;
36              
37             sub new($class, @params) {
38             $l->debug("Initializing '$class' with parameters: ".$l->flatten(@params)) if $l->is_debug();
39             my %self = validate(@params, {
40             baseUrl => 1,
41             credentials => 1,
42             });
43             my $s = bless(\%self, $class);
44              
45             $s->{credentials} = $s->_loadCredentials();
46              
47             $s->{ua} = LWP::UserAgent->new(
48             default_headers => HTTP::Headers->new(Authorization => $s->_authorization()),
49             );
50              
51             return $s;
52             }
53              
54             =head2 _authorization
55              
56             @RETURNS HTTP Basic authorization header content, eg. 'Basic QWxhZGRpbjpPcGVuU2VzYW1l'
57              
58             =cut
59              
60             sub _authorization($s) {
61             return 'Basic '.MIME::Base64::encode(Encode::encode('UTF-8', $s->credentials()), ''); #Turn $credentials into a byte/octet stream, and encode that as base64, with no eol
62             }
63              
64             =head2 request
65              
66             Make requests and deal with logging and error handling
67              
68             @RETURNS List of 0 - HTTP::Response
69             1 - HASHRef of response JSON payload
70             @THROWS Pootle::Exception::HTTP::MethodNotAllowed endpoint doesn't support the given method
71             @THROWS Pootle::Exception::HTTP::NotFound endpoint not found?
72              
73             =cut
74              
75             sub request($s, $verb, $apiUrl, $params) {
76             my $response = $s->ua->$verb($s->baseUrl.'/'.$apiUrl);
77             my $contentHash;
78             try {
79             $contentHash = $s->_getContent($response);
80             $l->trace("\$response: ".$s->_httpResponseToLoggableFromSuccess($response, $contentHash)) if $l->is_trace();
81             } catch {
82             if ($_ =~ /^malformed JSON string/) { #Presumably this is a JSON::XS issue
83             my $errorStr = $s->_httpResponseToLoggableFromFail($response);
84             $l->trace("\$response: ".$errorStr) if $l->is_trace();
85             Pootle::Exception::HTTP::MethodNotAllowed->throw(error => $errorStr) if $errorStr =~ /405 METHOD NOT ALLOWED$/sm;
86             Pootle::Exception::HTTP::NotFound->throw(error => $errorStr) if $errorStr =~ /404 Not Found$/sm;
87             Pootle::Exception::rethrowDefaults($errorStr);
88             }
89             Pootle::Exception::rethrowDefaults($_);
90             };
91             return ($response, $contentHash);
92             }
93              
94             =head2 _getContent
95              
96             @RETURNS HASHRef, Content's JSON payload decoded to Perl's internal UTF-8 representation
97              
98             =cut
99              
100             sub _getContent($s, $response) {
101             my $content = $response->content();
102             return JSON::XS->new->utf8->decode($content);
103             }
104              
105             sub _httpResponseToLoggableFromSuccess($s, $response, $contentHash) {
106             return join("\n",
107             $s->_httpResponseToLoggableHeader($response),
108             scalar(Data::Dumper->new([$contentHash],[])->Terse(1)->Indent(1)->Varname('')->Maxdepth(0)->Sortkeys(1)->Quotekeys(1)->Dump()),
109             );
110             }
111              
112             sub _httpResponseToLoggableFromFail($s, $response) {
113             return join("\n",
114             $s->_httpResponseToLoggableHeader($response),
115             $response->content(),
116             );
117             }
118              
119             sub _httpResponseToLoggableHeader($s, $response) {
120             my $status_line = $response->status_line;
121             my $proto = $response->protocol;
122             $status_line = "$proto $status_line" if $proto;
123             return join("\n", $status_line, $response->headers_as_string("\n"),''); #Includes empty line to signal the start of HTTP payload
124             }
125              
126             sub _loadCredentials($s) {
127             my $c = $s->credentials();
128             my $credentialsConfirmed;
129             my $file;
130             if (-e $c) { #This is a file
131             $file = $c;
132             $l->info("Loading credentials from file '$c'");
133             my @rows = File::Slurp::read_file( $c => { binmode => ':encoding(UTF-8)' } );
134             foreach my $row (@rows) {
135             if ($row =~ /^(.+):(.+)$/) {
136             $credentialsConfirmed = "$1:$2";
137             }
138             last;
139             }
140             }
141             else {
142             $credentialsConfirmed = $c;
143             }
144              
145             unless ($credentialsConfirmed && $credentialsConfirmed =~ /^(.+):(.+)$/) {
146             Pootle::Exception::Credentials->throw(error => "_loadCredentials():> Given credentials ".($file ? "from file '$file' " : "")."are malformed. Credentials must look like username:password, or point to a file with properly formatted credentials.");
147             }
148             return $credentialsConfirmed;
149             }
150              
151             ########## ### ###
152             ## ACCESSORS ### ###
153             ########## ### ###
154              
155             =head2 baseUrl
156              
157             @RETURNS String, the full url of the Pootle server we are interfacing with, eg. https://translate.koha-community.org
158              
159             =cut
160              
161             sub baseUrl($s) {
162             return $s->{baseUrl};
163             }
164              
165             =head2 credentials
166              
167             @RETURNS String, username:password
168              
169             =cut
170              
171             sub credentials($s) {
172             return $s->{credentials};
173             }
174              
175             =head2 ua
176              
177             @RETURNS L
178              
179             =cut
180              
181             sub ua($s) { return $s->{ua} }
182              
183             1;