File Coverage

blib/lib/Bing/ContentAPI.pm
Criterion Covered Total %
statement 21 107 19.6
branch 0 54 0.0
condition 0 19 0.0
subroutine 7 16 43.7
pod 4 9 44.4
total 32 205 15.6


line stmt bran cond sub pod time code
1             ##############################################################################
2             # Bing::ContentAPI
3             #
4             # Add, modify and delete products from the Bing Merchant Center platform via
5             # the Bing Ads Content API.
6             #
7             # https://docs.microsoft.com/bingads/shopping-content/
8             #
9             # Authentication is done via OAuth using Authorization Code Grant Flow
10             # https://docs.microsoft.com/bingads/guides/authentication-oauth
11             #
12             # AUTHOR
13             #
14             # Bill Gerrard
15             #
16             # VERSION HISTORY
17             #
18             # + v1.01 05/16/2018 dependency and documentation updates
19             # + v1.00 05/04/2018 initial release
20             #
21             # COPYRIGHT AND LICENSE
22             #
23             # Copyright (C) 2018 Bill Gerrard
24             #
25             # This program is free software; you can redistribute it and/or modify
26             # it under the same terms as Perl itself, either Perl version 5.20.2 or,
27             # at your option, any later version of Perl 5 you may have available.
28             #
29             # Disclaimer of warranty: This program is provided by the copyright holder
30             # and contributors "As is" and without any express or implied warranties.
31             # The implied warranties of merchantability, fitness for a particular purpose,
32             # or non-infringement are disclaimed to the extent permitted by your local
33             # law. Unless required by law, no copyright holder or contributor will be
34             # liable for any direct, indirect, incidental, or consequential damages
35             # arising in any way out of the use of the package, even if advised of the
36             # possibility of such damage.
37             #
38             ################################################################################
39              
40             package Bing::ContentAPI;
41              
42 1     1   71863 use strict;
  1         2  
  1         29  
43 1     1   5 use warnings;
  1         2  
  1         22  
44 1     1   5 use Carp;
  1         1  
  1         62  
45              
46 1     1   668 use JSON;
  1         12660  
  1         6  
47 1     1   641 use REST::Client;
  1         51408  
  1         36  
48 1     1   562 use HTML::Entities;
  1         5773  
  1         1121  
49              
50             our $VERSION = '1.01';
51              
52             sub new {
53 0     0 1   my ($class, $param) = @_;
54 0           my $self = {};
55              
56 0           foreach my $val (qw(merchant_id developer_token client_id redirect_uri refresh_token)) {
57 0   0       $self->{$val} = $param->{$val} || croak "param '$val' missing in new()";
58             }
59              
60 0 0         $self->{debug} = 1 if $param->{debug};
61              
62 0           refresh_access_token($self); # sets access_token, refresh_token
63              
64 0           $self->{rest} = init_rest_client($self);
65              
66 0           return bless $self, $class;
67             }
68              
69             sub get {
70 0     0 1   my $self = shift;
71 0 0         croak "Odd number of arguments for get()" if scalar(@_) % 2;
72 0           my $opt = {@_};
73 0           my $method = $self->prepare_method($opt);
74 0           return $self->request('GET', $method);
75             }
76              
77             sub post {
78 0     0 0   my $self = shift;
79 0 0         croak "Odd number of arguments for post()" if scalar(@_) % 2;
80 0           my $opt = {@_};
81 0           my $method = $self->prepare_method($opt);
82 0 0         $opt->{body} = encode_json $opt->{body} if $opt->{body};
83 0           return $self->request('POST', $method, $opt->{body});
84             }
85              
86             sub delete {
87 0     0 1   my $self = shift;
88 0 0         croak "Odd number of arguments for delete()" if scalar(@_) % 2;
89 0           my $opt = {@_};
90 0           my $method = $self->prepare_method($opt);
91 0           return $self->request('DELETE', $method);
92             }
93              
94             sub prepare_method {
95 0     0 0   my $self = shift;
96 0           my $opt = shift;
97              
98 0 0         $opt->{resource} = '' if $opt->{resource} eq 'custom';
99              
100 0 0 0       if ($opt->{resource} eq 'products' || $opt->{resource} eq 'catalogs'
101             ) {
102             # add merchant ID to request URL
103 0           $opt->{resource} = $self->{merchant_id} .'/'. $opt->{resource};
104              
105             # drop list/insert methods; these are for coding convenience only
106 0 0         $opt->{method} = '' if $opt->{method} eq 'list';
107 0 0         $opt->{method} = '' if $opt->{method} eq 'insert';
108              
109             # insert catalog ID to request URL for status
110 0 0         $opt->{method} = $opt->{id} .'/'. $opt->{method} if $opt->{method} eq 'status';
111              
112             # append product ID to end of request URL for get and delete
113 0 0         $opt->{method} = $opt->{id} if $opt->{method} =~ /get|delete/;
114             }
115              
116 0 0         push @{$opt->{params}}, ('dry-run','1') if $opt->{dryrun};
  0            
117 0 0         my $encoded_params = $self->{rest}->buildQuery($opt->{params}) if $opt->{params};
118              
119 0           my $method;
120 0 0         $method .= '/'. $opt->{resource} if $opt->{resource} ne '';
121 0 0         $method .= '/'. $opt->{method} if $opt->{method} ne '';
122 0 0         $method .= $encoded_params if $encoded_params;
123              
124 0           return $method;
125             }
126              
127             sub init_rest_client {
128 0     0 0   my $self = shift;
129 0           my $r = REST::Client->new();
130             ### https://docs.microsoft.com/bingads/shopping-content/manage-products
131 0           $r->setHost('https://content.api.bingads.microsoft.com/shopping/v9.1/bmc');
132 0           $r->addHeader('AuthenticationToken', $self->{access_token});
133 0           $r->addHeader('DeveloperToken', $self->{developer_token});
134 0           $r->addHeader('Content-type', 'application/json');
135 0           $r->addHeader('charset', 'UTF-8');
136 0           return $r;
137             }
138              
139             my $refresh_token_info = qq|################################################################################
140             This error may be caused by an invalid refresh token. Follow the procedure
141             to authorize app and obtain a valid refresh token.
142             https://docs.microsoft.com/bingads/guides/authentication-oauth#authorizationcode
143             ################################################################################
144             \n|;
145              
146             sub request {
147 0     0 0   my $self = shift;
148 0           my @command = @_;
149              
150 0 0         print join (' ', @command) . "\n" if $self->{debug};
151 0           my $rest = $self->{rest}->request(@command);
152              
153 0 0         unless ($rest->responseCode eq '200') {
154 0 0 0       if ($rest->responseCode eq '204' && $command[0] eq 'DELETE') {
    0          
155             # no-op: delete was successful
156             } elsif ($rest->responseCode eq '109') {
157             # AuthenticationTokenExpired error code (109), request new refresh token
158 0           $self->refresh_access_token();
159 0           $self->{rest} = $self->init_rest_client();
160 0           $rest = $self->{rest}->request(@command);
161             } else {
162 0 0         my $auth_error = ($rest->responseCode ne '401') ? '' : $refresh_token_info;
163 0           die("${auth_error}Error processing REST request:\n",
164             "Request: ", $rest->getHost , $command[1], "\n",
165             "Response Code: ", $rest->responseCode, "\n", $rest->responseContent, "\n");
166             }
167             }
168 0 0         print "Request Response: \n". $rest->responseContent if $self->{debug};
169              
170 0 0         my $response = $rest->responseContent ? decode_json $rest->responseContent : {};
171 0           return { code => $rest->responseCode, response => $response };
172             }
173              
174             sub get_access_token {
175 0     0 0   my $self = shift;
176 0 0         croak "Odd number of arguments for get_access_token()" if scalar(@_) % 2;
177 0           my $opt = {@_};
178              
179 0           my $bapiTokenURI = 'https://login.live.com/oauth20_token.srf';
180              
181             croak "missing grant_type" unless $opt->{grant_type}
182             || $opt->{grant_type} eq 'authorization_code'
183 0 0 0       || $opt->{grant_type} eq 'refresh_token';
      0        
184              
185 0 0         if ($opt->{grant_type} eq 'authorization_code') {
186 0           $opt->{ctype} = 'code';
187 0   0       $opt->{cval} = $opt->{code} || '';
188             } else {
189 0           $opt->{ctype} = 'refresh_token';
190 0   0       $opt->{cval} = $opt->{refresh_token} || '';
191             }
192              
193 0           my $ua = LWP::UserAgent->new();
194             my $response = $ua->post($bapiTokenURI, {
195             client_id => $self->{client_id},
196             redirect_uri => $self->{redirect_uri},
197             grant_type => $opt->{grant_type},
198             $opt->{ctype} => $opt->{cval},
199 0           });
200              
201 1     1   646 use Data::Dumper;
  1         6842  
  1         226  
202 0           print Dumper $response;
203 0           die;
204             }
205              
206             sub refresh_access_token {
207 0     0 1   my $self = shift;
208             # foreach my $val (qw(client_id redirect_uri refresh_token)) {
209             # $self->{$val} && $self->{$val} ne '' || croak "'$val' not defined for refresh_access_token()";
210             # }
211              
212 0           my $bapiTokenURI = 'https://login.live.com/oauth20_token.srf';
213              
214 0           my $ua = LWP::UserAgent->new();
215             my $response = $ua->post($bapiTokenURI, {
216             grant_type => 'refresh_token',
217             client_id => $self->{client_id},
218             redirect_uri => $self->{redirect_uri},
219             refresh_token => $self->{refresh_token},
220 0           });
221              
222 0 0         unless($response->is_success()) {
223 0           die("Error receiving access token:\n", $response->code, "\n", $response->content, "\n");
224             }
225              
226 0           my $data = decode_json $response->content;
227 0           $self->{access_token} = $data->{access_token};
228 0           $self->{refresh_token} = $data->{refresh_token};
229             }
230              
231             1;
232              
233             __END__