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