1#!/usr/local/bin/perl
2# This Source Code Form is subject to the terms of the Mozilla Public
3# License, v. 2.0. If a copy of the MPL was not distributed with this
4# file, You can obtain one at http://mozilla.org/MPL/2.0/.
5#
6# This Source Code Form is "Incompatible With Secondary Licenses", as
7# defined by the Mozilla Public License, v. 2.0.
8
9=head1 NAME
10
11bz_webservice_demo.pl - Show how to talk to Bugzilla via XMLRPC
12
13=head1 SYNOPSIS
14
15C<bz_webservice_demo.pl [options]>
16
17C<bz_webservice_demo.pl --help> for detailed help
18
19=cut
20
21use 5.10.1;
22use strict;
23use warnings;
24
25use lib qw(lib);
26use Getopt::Long;
27use Pod::Usage;
28use File::Basename qw(dirname);
29use File::Spec;
30use XMLRPC::Lite;
31
32# If you want, say “use Bugzilla::WebService::Constants” here to get access
33# to Bugzilla's web service error code constants.
34# If you do this, remember to issue a “use lib” pointing to your Bugzilla
35# installation directory, too.
36
37my $help;
38my $Bugzilla_uri;
39my $Bugzilla_login;
40my $Bugzilla_password;
41my $Bugzilla_restrict;
42my $Bugzilla_token;
43my $bug_id;
44my $product_name;
45my $create_file_name;
46my $legal_field_values;
47my $add_comment;
48my $private;
49my $work_time;
50my $fetch_extension_info = 0;
51my $debug;
52
53GetOptions('help|h|?'       => \$help,
54           'uri=s'          => \$Bugzilla_uri,
55           'login:s'        => \$Bugzilla_login,
56           'password=s'     => \$Bugzilla_password,
57           'restrictlogin!' => \$Bugzilla_restrict,
58           'bug_id:s'       => \$bug_id,
59           'product_name:s' => \$product_name,
60           'create:s'       => \$create_file_name,
61           'field:s'        => \$legal_field_values,
62           'comment:s'      => \$add_comment,
63           'private:i'      => \$private,
64           'worktime:f'     => \$work_time,
65           'extension_info' => \$fetch_extension_info,
66           'debug'          => \$debug
67          ) or pod2usage({'-verbose' => 0, '-exitval' => 1});
68
69=head1 OPTIONS
70
71=over
72
73=item --help, -h, -?
74
75Print a short help message and exit.
76
77=item --uri
78
79URI to Bugzilla's C<xmlrpc.cgi> script, along the lines of
80C<http://your.bugzilla.installation/path/to/bugzilla/xmlrpc.cgi>.
81
82=item --login
83
84Bugzilla login name. Specify this together with B<--password> in order to log in.
85
86Specify this without a value in order to log out.
87
88=item --password
89
90Bugzilla password. Specify this together with B<--login> in order to log in.
91
92=item --restrictlogin
93
94Gives access to Bugzilla's "Bugzilla_restrictlogin" option.
95Specify this option while logging in to restrict the login token to be
96only valid from the IP address which called
97Don't specify this option to do the same thing as unchecking the box.
98
99See Bugzilla's restrictlogin parameter for details.
100
101=item --bug_id
102
103Pass a bug ID to have C<bz_webservice_demo.pl> do some bug-related test calls.
104
105=item --product_name
106
107Pass a product name to have C<bz_webservice_demo.pl> do some product-related
108test calls.
109
110=item --create
111
112Specify a file that contains settings for the creating of a new bug.
113
114=item --field
115
116Pass a field name to get legal values for this field. It must be either a
117global select field (such as bug_status, resolution, rep_platform, op_sys,
118priority, bug_severity) or a custom select field.
119
120=item --comment
121
122A comment to add to a bug identified by B<--bug_id>. You must also pass a B<--login>
123and B<--password> to log in to Bugzilla.
124
125=item --private
126
127An optional non-zero value to specify B<--comment> as private.
128
129=item --worktime
130
131An optional double precision number specifying the work time for B<--comment>.
132
133=item --extension_info
134
135If specified on the command line, the script returns the information about the
136extensions that are installed.
137
138=item --debug
139
140Enable tracing at the debug level of XMLRPC requests and responses.
141
142=back
143
144=head1 DESCRIPTION
145
146=cut
147
148pod2usage({'-verbose' => 1, '-exitval' => 0}) if $help;
149_syntaxhelp('URI unspecified') unless $Bugzilla_uri;
150
151# We will use this variable for SOAP call results.
152my $soapresult;
153
154# We will use this variable for function call results.
155my $result;
156
157=head2 Initialization
158
159Using the XMLRPC::Lite class, you set up a proxy, as shown in this script.
160Bugzilla's XMLRPC URI ends in C<xmlrpc.cgi>, so your URI looks along the lines
161of C<http://your.bugzilla.installation/path/to/bugzilla/xmlrpc.cgi>.
162
163=cut
164
165my $proxy = XMLRPC::Lite->proxy($Bugzilla_uri);
166
167=head2 Debugging
168
169Enable tracing at the debug level of XMLRPC requests and responses if requested.
170
171=cut
172
173if ($debug) {
174   $proxy->import(+trace => 'debug');
175}
176
177=head2 Checking Bugzilla's version
178
179To make sure the Bugzilla you're connecting to supports the methods you wish to
180call, you may want to compare the result of C<Bugzilla.version> to the
181minimum required version your application needs.
182
183=cut
184
185$soapresult = $proxy->call('Bugzilla.version');
186_die_on_fault($soapresult);
187print 'Connecting to a Bugzilla of version ' . $soapresult->result()->{version} . ".\n";
188
189=head2 Checking Bugzilla's timezone
190
191To make sure that you understand the dates and times that Bugzilla returns to you, you may want to call C<Bugzilla.timezone>.
192
193=cut
194
195$soapresult = $proxy->call('Bugzilla.timezone');
196_die_on_fault($soapresult);
197print 'Bugzilla\'s timezone is ' . $soapresult->result()->{timezone} . ".\n";
198
199=head2 Logging In and Out
200
201=head3 Using Bugzilla's Environment Authentication
202
203Use a
204C<http://login:password@your.bugzilla.installation/path/to/bugzilla/xmlrpc.cgi>
205style URI.
206You don't log out if you're using this kind of authentication.
207
208=head3 Using Bugzilla's CGI Variable Authentication
209
210Use the C<User.login> and C<User.logout> calls to log in and out, as shown
211in this script.
212
213The C<Bugzilla_restrictlogin> parameter is optional.
214If omitted, Bugzilla's defaults apply (as specified by its C<restrictlogin>
215parameter).
216
217=cut
218
219if (defined($Bugzilla_login)) {
220    if ($Bugzilla_login ne '') {
221        # Log in.
222        $soapresult = $proxy->call('User.login',
223                                   { login => $Bugzilla_login,
224                                     password => $Bugzilla_password,
225                                     restrict_login => $Bugzilla_restrict } );
226        $Bugzilla_token = $soapresult->result->{token};
227        _die_on_fault($soapresult);
228        print "Login successful.\n";
229    }
230    else {
231        # Log out.
232        $soapresult = $proxy->call('User.logout');
233        _die_on_fault($soapresult);
234        print "Logout successful.\n";
235    }
236}
237
238=head2 Getting Extension Information
239
240Returns all the information any extensions have decided to provide to the webservice.
241
242=cut
243
244if ($fetch_extension_info) {
245    $soapresult = $proxy->call('Bugzilla.extensions', {token => $Bugzilla_token});
246    _die_on_fault($soapresult);
247    my $extensions = $soapresult->result()->{extensions};
248    foreach my $extensionname (keys(%$extensions)) {
249        print "Extension '$extensionname' information\n";
250        my $extension = $extensions->{$extensionname};
251        foreach my $data (keys(%$extension)) {
252            print '  ' . $data . ' => ' . $extension->{$data} . "\n";
253        }
254    }
255}
256
257=head2 Retrieving Bug Information
258
259Call C<Bug.get> with the ID of the bug you want to know more of.
260The call will return a C<Bugzilla::Bug> object.
261
262=cut
263
264if ($bug_id) {
265    $soapresult = $proxy->call('Bug.get', { ids => [$bug_id], token => $Bugzilla_token});
266    _die_on_fault($soapresult);
267    $result = $soapresult->result;
268    my $bug = $result->{bugs}->[0];
269    foreach my $field (keys(%$bug)) {
270        my $value = $bug->{$field};
271        if (ref($value) eq 'HASH') {
272            foreach (keys %$value) {
273                print "$_: " . $value->{$_} . "\n";
274            }
275        }
276        else {
277            print "$field: $value\n";
278        }
279    }
280}
281
282=head2 Retrieving Product Information
283
284Call C<Product.get> with the name of the product you want to know more of.
285The call will return a C<Bugzilla::Product> object.
286
287=cut
288
289if ($product_name) {
290    $soapresult = $proxy->call('Product.get', {'names' => [$product_name], token => $Bugzilla_token});
291    _die_on_fault($soapresult);
292    $result = $soapresult->result()->{'products'}->[0];
293
294    # Iterate all entries, the values may be scalars or array refs with hash refs.
295    foreach my $key (sort(keys %$result)) {
296      my $value = $result->{$key};
297
298      if (ref($value)) {
299        my $counter = 0;
300        foreach my $hash (@$value) {
301          while (my ($innerKey, $innerValue) = each %$hash) {
302            print "$key.$counter.$innerKey: $innerValue\n";
303          }
304          ++$counter;
305        }
306      }
307      else {
308        print "$key: $value\n"
309      }
310    }
311}
312
313=head2 Creating A Bug
314
315Call C<Bug.create> with the settings read from the file indicated on
316the command line. The file must contain a valid anonymous hash to use
317as argument for the call to C<Bug.create>.
318The call will return a hash with a bug id for the newly created bug.
319
320=cut
321
322if ($create_file_name) {
323    my $bug_fields = do "$create_file_name";
324    $bug_fields->{Bugzilla_token} = $Bugzilla_token;
325    $soapresult = $proxy->call('Bug.create', \%$bug_fields);
326    _die_on_fault($soapresult);
327    $result = $soapresult->result;
328
329    if (ref($result) eq 'HASH') {
330        foreach (keys(%$result)) {
331            print "$_: $$result{$_}\n";
332        }
333    }
334    else {
335        print "$result\n";
336    }
337
338}
339
340=head2 Getting Legal Field Values
341
342Call C<Bug.legal_values> with the name of the field (including custom
343select fields). The call will return a reference to an array with the
344list of legal values for this field.
345
346=cut
347
348if ($legal_field_values) {
349    $soapresult = $proxy->call('Bug.legal_values', {field => $legal_field_values, token => $Bugzilla_token} );
350    _die_on_fault($soapresult);
351    $result = $soapresult->result;
352
353    print join("\n", @{$result->{values}}) . "\n";
354}
355
356=head2 Adding a comment to a bug
357
358Call C<Bug.add_comment> with the bug id, the comment text, and optionally the number
359of hours you worked on the bug, and a boolean indicating if the comment is private
360or not.
361
362=cut
363
364if ($add_comment) {
365    if ($bug_id) {
366        $soapresult = $proxy->call('Bug.add_comment', {id => $bug_id,
367            comment => $add_comment, private => $private, work_time => $work_time, token => $Bugzilla_token});
368        _die_on_fault($soapresult);
369        print "Comment added.\n";
370    }
371    else {
372        print "A --bug_id must be supplied to add a comment.";
373    }
374}
375
376=head1 NOTES
377
378=head2 Character Set Encoding
379
380Make sure that your application either uses the same character set
381encoding as Bugzilla does, or that it converts correspondingly when using the
382web service API.
383By default, Bugzilla uses UTF-8 as its character set encoding.
384
385=head2 Format For Create File
386
387The create format file is a piece of Perl code, that should look something like
388this:
389
390    {
391        product     => "TestProduct",
392        component   => "TestComponent",
393        summary     => "TestBug - created from bz_webservice_demo.pl",
394        version     => "unspecified",
395        description => "This is a description of the bug... hohoho",
396        op_sys      => "All",
397        platform    => "All",
398        priority    => "P4",
399        severity    => "normal"
400    };
401
402=head1 SEE ALSO
403
404There are code comments in C<bz_webservice_demo.pl> which might be of further
405help to you.
406
407=cut
408
409sub _die_on_fault {
410    my $soapresult = shift;
411
412    if ($soapresult->fault) {
413        my ($package, $filename, $line) = caller;
414        die $soapresult->faultcode . ' ' . $soapresult->faultstring .
415            " in SOAP call near $filename line $line.\n";
416    }
417}
418
419sub _syntaxhelp {
420    my $msg = shift;
421
422    print "Error: $msg\n";
423    pod2usage({'-verbose' => 0, '-exitval' => 1});
424}
425