3 $WWW::Bugzilla::VERSION = '1.5';
8 use Fatal qw(:void open opendir);
9 use Carp qw(croak carp);
11 use constant FIELDS => qw( bugzilla_version bugzilla_version_minor version component status resolution dup_id assigned_to summary bug_number description os platform severity priority cc url add_cc target_milestone status_whiteboard keywords depends_on blocks additional_comments );
13 my %new_field_map = ( product => 'product',
15 component => 'component',
16 assigned_to => 'assigned_to',
17 summary => 'short_desc',
18 description => 'comment',
20 platform => 'rep_platform',
21 severity => 'bug_severity',
22 priority => 'priority',
24 url => 'bug_file_loc' );
26 my %other_field_map = (
27 resolution => 'resolution_knob_5',
30 my %update_field_map = (product => 'product',
31 # bug_number => 'id', # this cannot be updated
32 platform => 'rep_platform',
35 component => 'component',
37 # cc => 'cc', # this field should not be updated, use add_cc
39 priority => 'priority',
40 severity => 'bug_severity',
41 target_milestone => 'target_milestone',
42 url => 'bug_file_loc',
43 summary => 'short_desc',
44 status_whiteboard => 'status_whiteboard',
45 keywords => 'keywords',
46 depends_on => 'dependson',
48 additional_comments => 'comment' );
52 WWW::Bugzilla - Handles submission/update of bugzilla bugs via WWW::Mechanize.
59 my $bz = WWW::Bugzilla->new( server => 'www.mybugzilla.com',
60 email => 'buguser@bug.com',
61 password => 'mypassword' );
63 # enter info into some fields and save new bug
65 # get list of available version choices
66 my @versions = $bz->available('version');
69 $bz->version( $versions[0] );
71 # get list of available products
72 my @products = $bz->available('product');
75 $bz->product( $products[0] );
77 # get list of components available
78 my @components = $bz->available('component');
81 $bz->component( $components[0] );
83 # optionally do the same for platform, os, priority, severity.
85 $bz->assigned_to( 'joeschmoe@whatever.com' );
86 $bz->summary( $some_text );
87 $bz->description( $some_more_text );
89 # submit bug, returning new bug number
90 my $bug_number = $bz->commit;
92 # all of the above could have been done in a much easier
93 # way, had we known what values to use. See below:
95 my $bz = WWW::Bugzilla->new( server => 'www.mybugzilla.com',
96 email => 'buguser@bug.com',
97 password => 'mypassword'
99 product => 'MyProduct',
101 assigned_to => 'joeschmoe@whatever.com',
102 summary => $some_text,
103 description => $some_more_text);
105 my $bug_number = $bz->commit;
107 # Below is an example of how one would update a bug.
109 my $bz = WWW::Bugzilla->new( server => 'www.mybugzilla.com',
110 email => 'buguser@bug.com',
111 password => 'mypassword'
114 # show me the chosen component
115 my $component = $bz->component;
118 $bz->component( 'Test Failures' );
120 $bz->add_cc( 'me@me.org' );
122 $bz->add_attachment( filepath => '/home/me/file.txt',
123 description => 'description text',
125 comment => 'comment text here' );
127 $bz->additional_comments( "comments here");
129 # below are examples of changing bug status
130 $bz->change_status("assigned");
131 $bz->change_status("fixed");
132 $bz->change_status("later");
133 $bz->mark_as_duplicate("12");
134 $bz->reassign("someone@else.com");
140 WWW::Bugzilla currently provides an API to posting new Bugzilla
141 bugs, as well as updating existing Bugzilla bugs.
151 Initialize WWW::Bugzilla object. If bug_number is passed in,
152 will initialize as existing bug. Will croak() unless the
153 Bugzilla login page on server specified returns a 200 or 404.
154 new() supports the following name-value parameters.
158 =item server (required)
160 URL of the bugzilla server you wish to interface with. Do not
161 place http:// or https:// in front of the url (see 'use_ssl' option
164 =item email (required)
166 Your email address used by bugzilla, in other words your
169 =item password (required)
173 =item use_ssl (optional)
175 If set, will use https:// protocol, defaults to http://.
177 NOTE: This option requires Crypt::SSLeay.
181 Bugzilla product name, required if entering new bug
184 =item bug_number (optional)
186 If you mean to update an existing bug (not create a new one)
187 include a valid bug number here.
189 =item version component status assigned_to resolution dup_id assigned_to summary bug_number description os platform severity priority cc url add_cc target_milestone status_whiteboard keywords depends_on blocks additional_comments
191 These are fields that can be initialized on new(), useful for new bugs.
192 Please note that some of these fields apply only to bugs being updated,
193 and if you set them here, they will be overridden if the value is already
194 set in the actual bug on the server. These fields also have thier own
195 get/set methods (see below).
201 use Class::MethodMaker
202 new_with_init => 'new',
203 new_hash_init => 'hash_init',
204 get_set => [ FIELDS ];
210 croak("'server', 'email', and 'password' are all required arguments.") if ( (not $args{server}) or (not $args{email}) or (not $args{password}) );
212 # croak("'product' required for new bug.") if ( (not $args{product}) and (not $args{bug_number}) );
213 $self->{'product'} = $args{'product'} if (defined($args{'product'}));
215 $self->{mech} = WWW::Mechanize->new();
217 $self->{protocol} = delete($args{use_ssl}) ? 'https' : 'http';
219 $self->{server} = $args{server};
220 $self->_login( delete $args{server}, delete $args{email}, delete $args{password});
223 $self->hash_init(%args);
225 if ($self->{bug_number}) {
226 $self->_get_update_page();
227 } elsif ($self->{product}) {
228 $self->_get_new_page();
231 $self->check_error();
238 my $mech = $self->{mech};
239 my $new_page = $self->{protocol}.'://'.$self->{server}.'/enter_bug.cgi?product='.$self->{product};
240 $mech->get($new_page);
241 $self->check_error();
242 $mech->form_name('Create') if ($self->bugzilla_version == 3);
244 # bail unless OK or Redirect happens
245 croak("Cannot open page $new_page") unless ( ($mech->status == '200') or ($mech->status == '404') );
248 sub _get_update_page {
251 my $mech = $self->{mech};
252 $self->_get_form_by_field('quicksearch');
253 $mech->field('quicksearch', $self->{bug_number});
255 $self->check_error();
257 $mech->form_name("changeform");
258 # set fields to chosen values
259 foreach my $field ( keys %update_field_map ) {
260 if ($mech->current_form->find_input($update_field_map{$field})) {
261 $self->{$field} = $mech->current_form->value( $update_field_map{$field} );
263 # warn "# Couldn't find $field";
268 # based on the current page, set the current form to the first form with a specified field
269 sub _get_form_by_field {
270 my ($self, $field) = @_;
271 croak("invalid field") if !$field;
273 my $mech = $self->{mech};
275 foreach my $form ($mech->forms()) {
276 if ($form->find_input($field)) {
277 $mech->form_number($i);
282 croak("No form with the field $field available");
287 my ($server, $email, $password) = @_;
289 my $mech = $self->{mech};
291 my $login_page = $self->{protocol}.'://'.$server.'/query.cgi?GoAheadAndLogIn=1';
293 $mech->get( $login_page );
295 # bail unless OK or Redirect happens
296 croak("Cannot open page $login_page") unless ( ($mech->status == '200') or ($mech->status == '404') );
298 $self->_get_form_by_field('Bugzilla_login');
299 $mech->field('Bugzilla_login', $email);
300 $mech->field('Bugzilla_password', $password);
301 $mech->submit_form();
304 $mech->get($self->{protocol}.'://'.$server.'/');
306 if ($mech->content() =~ /<span>Version (\d+)\.(\d+)(\.\d+)?\+?<\/span>/) {
307 $self->bugzilla_version($1);
308 $self->bugzilla_version_minor($2);
309 } elsif ($mech->content() =~ /<p class="header_addl_info">version (\d+)\./smi) {
310 $self->bugzilla_version($1);
312 croak("Unable to verify bugzilla version.");
315 if ($self->bugzilla_version > 2) {
316 $update_field_map{'status'} = 'bug_status';
317 $other_field_map{'resolution'} = 'resolution';
321 =item product() version() component() status() assigned_to() resolution() dup_id() assigned_to() summary() bug_number() description() os() platform() severity() priority() cc() url() add_cc() target_milestone() status_whiteboard() keywords() depends_on() blocks() additional_comments()
323 get/set the value of these bug fields. Some apply only to new bugs, some
324 only to bugs being updated. commit() must be called to save these
329 Returns list of available options for field requested. Below are known
345 my $field_choice = shift;
346 my $mech = $self->{mech};
348 # we handle product seperately because bugzilla requires it to be handled
349 # seperately on bug creation
350 if ('product' eq lc($field_choice)) {
351 return $self->get_products();
354 # make sure that we've set a product before we do any of the other stuff
355 croak("available() needs a valid product to be specified") if not $self->{'product'};
357 # note that we are using %new_field map regardless if this is a new bug
358 # or not. this should work, as these fields should be the same for
359 # both new and old, but look here if problems occur!
361 if (my $item = $mech->current_form->find_input( $new_field_map{$field_choice} )) {
362 return $item->possible_values();
370 Set the Product for the bug
375 my ($self, $product) = @_;
378 $self->{'product'} = $product;
379 if ($self->{bug_number}) {
380 $self->_get_update_page();
381 } elsif ($self->{'product'}) {
382 $self->_get_new_page();
385 return ($self->{'product'});
391 Mark bug being updated as reassigned to another user. Takes email
392 address as parameter. Status/resolution will not be updated
393 until commit() is called.
401 croak("reassign() needs a bug number passed in as a parameter") if not $email;
403 croak("reassign() may not be called until the bug is committed for the first time") if not $self->{bug_number};
405 $self->{status} = 'reassign';
406 $self->{assigned_to} = $email;
409 =item mark_as_duplicate()
411 Mark bug being updated as duplicate of another bug number.
412 Takes bug number as argument.
413 Status/resolution will not be updated until commit() is called.
417 sub mark_as_duplicate {
421 croak("mark_as_duplicate() needs a bug number passed in as a parameter") if not $dup_id;
423 croak("mark_as_duplicate() may not be called until the bug is committed for the first time") if not $self->{bug_number};
425 $self->{status} = 'RESOLVED';
426 $self->{resolution} = 'DUPLICATE';
427 $self->{dup_id} = $dup_id;
430 =item change_status()
432 Change status of bug being updated. Status/resolution will not
433 be updated until commit() is called. The following are valid
434 options (case-insensitive):
450 my ($self, $status) = @_;
452 croak("change_status() may not be called until the bug is committed for the first time") if not $self->{bug_number};
454 $status = uc($status);
457 'ASSIGNED' => 'accept',
458 'REOPENED' => 'reopen',
459 'VERIFIED' => 'verify',
473 croak ("$status is not a valid status.") if not ($resolution{$status} or $status{$status});
475 if ($status{$status}) {
476 $self->{status} = $status;
477 $self->{resolution} = '';
480 $self->{status} = "RESOLVED";
481 $self->{resolution} = $status;
487 =item add_attachment()
489 Adds attachment to existing bug - will not work for new
490 bugs. Below are available params:
500 description (required)
504 is_patch (optional boolean)
508 content_type - Autodetected if not defined.
516 finished - will not return object to update form if set (optional boolean)
525 my $mech = $self->{mech};
527 croak("add_attachment() may not be called until the bug is committed for the first time") if not $self->{bug_number};
529 croak("You must include a filepath and description.") unless ($args{filepath} and $args{description});
531 my $attach_page = $self->{protocol}.'://'.$self->{server}.'/attachment.cgi?bugid='.$self->{bug_number}.'&action=enter';
533 $mech->get( $attach_page );
534 $self->check_error();
535 $mech->form_name('entryform');
536 $mech->field( 'data', $args{'filepath'} );
537 $mech->field( 'description', $args{description} );
538 $mech->field( 'comment', $args{comment} ) if $args{comment};
539 $mech->field( 'ispatch', 1 ) if $args{'is_patch'};
541 if ($args{'bigfile'}) {
542 if ($mech->current_form->find_input('bigfile', 'checkbox', 0)) {
543 $mech->tick('bigfile', 'bigfile');
545 croak('Bigfile support is not available');
548 if ( $args{content_type} ) {
549 $mech->field( 'contenttypemethod', 'manual' );
550 $mech->field( 'contenttypeentry', $args{content_type} );
552 $mech->field( 'contenttypemethod', 'autodetect' );
555 $mech->submit_form();
556 $self->check_error();
558 if ($mech->content =~ /created/i) {
559 my $link = $mech->find_link(text_regex => qr/^Attachment #\d+$/);
560 my $title = $link->attrs()->{'title'};
562 if ($title ne "'" . $args{'description'} . "'" && $title ne $args{'description'}) {
563 croak('attachment not created');
565 if ($link->text =~ /^Attachment #(\d+)$/) {
568 croak('attachment not created');
572 $self->_get_update_page() unless ($args{finished});
576 =item list_attachments()
578 Lists attachments that are attached to an existing bug - will not work for new bugs.
582 sub list_attachments {
584 my $mech = $self->{mech};
586 croak("list_attachments() may not be called until the bug is committed for the first time") if not $self->{bug_number};
588 my $bug_page = $self->{protocol}.'://'.$self->{server}.'/show_bug.cgi?id='.$self->{bug_number};
589 $mech->get($bug_page);
590 $self->check_error();
594 foreach my $link ($mech->find_all_links(url_regex => qr/attachment\.cgi\?id=\d+$/)) {
595 if ($link->url() =~ /^attachment.cgi\?id=(\d+)$/) {
597 next if ($seen{$id});
599 my $i = $link->url();
601 my $re = '<a(?: name="a\d+")? href="' . $i . '"(?:\s*title="View the content of the attachment">\s*<b>|>)?<span class="(bz_obsolete)">';
602 my $obsolete = ($mech->content() =~ /$re/smi) ? 1 : 0;
603 push (@attachments, { id => $id, name => $link->text(), obsolete => $obsolete });
605 croak("WWW::Mechanize find_all_links gave us a bogus URL");
608 return (@attachments);
611 =item get_attachment()
613 Get the specified attachment from an existing bug - will not work for new bugs.
620 my $mech = $self->{mech};
622 croak("get_attachment() may not be called until the bug is committed for the first time") if not $self->{bug_number};
624 croak("You must provide either the 'id' or 'name' of the attachment you wish to retreive") unless ($args{id} || $args{name});
626 my $bug_page = $self->{protocol}.'://'.$self->{server}.'/show_bug.cgi?id='.$self->{bug_number};
627 $mech->get($bug_page);
628 $self->check_error();
632 @links = $mech->find_all_links( url => 'attachment.cgi?id=' . $args{'id'} );
633 } elsif ($args{'name'}) {
634 @links = $mech->find_all_links( text => $args{'name'} );
635 if (scalar(@links) > 1) {
636 carp('multiple attachments have the same name, returning the first one');
640 croak('No such attachment') if (!@links);
641 $mech->get($links[0]);
642 return $mech->content();
645 =item obsolete_attachment()
647 Mark the specified attachment obsolete. - will not work for new bugs.
651 sub obsolete_attachment {
654 my $mech = $self->{mech};
656 croak("obsolete_attachment() may not be called until the bug is committed for the first time") if not $self->{bug_number};
658 croak("You must provide either the 'id' or 'name' of the attachment you wish to obsolete") unless ($args{id} || $args{name});
660 my $bug_page = $self->{protocol}.'://'.$self->{server}.'/show_bug.cgi?id='.$self->{bug_number};
661 $mech->get($bug_page);
662 $self->check_error();
666 @links = $mech->find_all_links( url => 'attachment.cgi?id=' . $args{'id'} );
667 } elsif ($args{'name'}) {
668 @links = $mech->find_all_links( text => $args{'name'} );
669 if (scalar(@links) > 1) {
670 carp('multiple attachments have the same name, returning the first one');
673 croak('No such attachment') if (!@links);
674 $links[0]->[0] = $links[0]->[0] . '&action=edit';
675 $links[0]->[5]->{'href'} = $links[0]->[5]->{'href'} . '&action=edit';
676 $mech->get($links[0]);
677 $mech->form_with_fields('id', 'action', 'contenttypemethod');
678 $mech->tick("isobsolete", 1);
680 return $mech->content();
686 Submits bugzilla new or update form. Returns bug_number. Optionally
687 takes parameter finished- if set will you are done updating the bug,
688 and wil not return you to the update page.
695 my $mech = $self->{mech};
697 # print $mech->uri() . "\n";
698 unless ( ( $mech->content() =~ /a href="index\.cgi\?logout=1">/ ) or ( $mech->content() =~ /a href="relogin.cgi">/ ) ) {
699 croak("should be logged in to commit bugs");
702 if ($self->{bug_number}) {
704 if ($self->bugzilla_version() > 2) {
705 if ($self->{resolution}) {
706 $mech->field($update_field_map{'status'}, $self->{'status'});
707 $mech->field($other_field_map{'resolution'}, $self->{resolution});
708 $self->{resolution} = undef;
709 $self->{status} = undef;
710 } elsif ($self->{status}) {
711 $mech->field('bug_status', $self->{'status'});
712 $self->{resolution} = undef;
713 $self->{status} = undef;
716 if ($self->{resolution}) {
717 $mech->field($update_field_map{'status'}, 'resolve');
718 $mech->field($other_field_map{'resolution'}, $self->{resolution});
719 $self->{resolution} = undef;
720 $self->{status} = undef;
721 } elsif ($self->{status}) {
722 $mech->field($update_field_map{'status'}, $self->{status});
723 $self->{status} = undef;
727 if ($self->{dup_id}) {
728 $mech->field('dup_id', $self->{dup_id});
729 $self->{dup_id} = undef;
731 if ($self->{assigned_to}) {
732 $mech->field('assigned_to', $self->{assigned_to});
733 $self->{assigned_to} = undef;
735 foreach my $field ( keys %update_field_map ) {
737 if (!$mech->current_form->find_input($update_field_map{$field})) {
738 # warn "# $field is missing";
743 next if $mech->current_form->find_input($update_field_map{$field})->type eq 'hidden';
744 $mech->field( $update_field_map{$field}, $self->{$field} ) if defined($self->{$field});
747 foreach my $field ( keys %new_field_map ) {
748 if ($mech->current_form->find_input($new_field_map{$field})) {
749 # if field is defined and it has changed
750 if ( defined($self->{$field}) ) {
751 $mech->field( $new_field_map{$field}, $self->{$field} ) if ($mech->current_form->value($new_field_map{$field}) ne $self->{$field});
757 # delete the comment such that we don't reuse the same comment again accidentally.
758 delete($self->{'comment'});
760 $mech->submit_form();
762 # 3.3+ token checking
763 if ($mech->content() =~ /You submitted changes to process_bug\.cgi with an invalid/) {
764 $mech->form_name('check');
765 $mech->submit_form();
769 $self->check_error();
770 if (!$self->{bug_number}) {
771 if ($mech->content() =~ /<h2>Bug (\d+) has been added to the database/) {
772 $self->{bug_number} = $1;
773 } elsif ($mech->content() =~ />Bug (\d+)<\/a><\/i> has been added to the database<\/dt>/) {
774 $self->{bug_number} = $1;
775 } elsif ($mech->content() =~ /Bug (\d+) Submitted</) {
776 $self->{bug_number} = $1;
777 } elsif ($mech->content() =~ /Bug (\d+) Submitted</) {
778 $self->{bug_number} = $1;
780 # warn $mech->content();
781 croak("bug was not saved");
784 $self->_get_update_page() unless ($args{finished});
786 return $self->{bug_number};
791 Checks if an error was given, croaking if it did.
797 my $mech = $self->{mech};
799 if ($mech->content() =~ /<td bgcolor="#ff0000">[\s\r\n]*<font size="\+2">[\s\r\n]*(.*?)[\s\r\n]*<\/font>[\s\r\n]*<\/td>/smi) {
801 } elsif ($mech->content() =~ /<td id="error_msg" class="throw_error">\s*(.*?)\s*<\/td>/smi) {
803 } elsif ($mech->content() =~ /<div class="throw_error">\s*(.*?)<\/div>/smi) {
808 =item get_products ()
810 Gets a list of products
816 my $mech = $self->{mech};
818 my $url = $self->{protocol}.'://'.$self->{server}.'/enter_bug.cgi';
820 if ($self->bugzilla_version == 3) {
821 $url .= '?classification=__all';
824 $self->check_error();
827 foreach my $product ($mech->find_all_links( url_regex => qr/enter_bug.cgi\?product=/)) {
828 push (@products, $product->text());
837 Lists comments made on an existing bug - will not work for new bugs.
843 croak("get_comments() may not be called until the bug is committed for the first time") if not $self->{bug_number};
845 my $mech = $self->{mech};
846 my $bug_page = $self->{protocol}.'://'.$self->{server}.'/show_bug.cgi?id='.$self->{bug_number};
847 $mech->get($bug_page);
848 $self->check_error();
851 my $content = $mech->content();
852 while ($content =~ m/<pre id="comment_text_\d+">(.*?)<\/pre>/smg) {
855 push (@comments, $comment);
859 while ($content =~ m/<pre class="bz_comment_text" id="comment_text_\d+">\s*(.*?)<\/pre>/smg) {
862 push (@comments, $comment);
870 =head1 BUGS, IMPROVEMENTS
872 There may well be bugs in this module. Using it as I have, I just have not run
873 into any. In addition, this module does not support ALL of Bugzilla's
874 features. I will consider any patches or improvements, just send me an email
875 at the address listed below.
880 Brian Caswell, bmc@shmoo.com
882 Originally written by:
883 Matthew C. Vella, the_mcv@yahoo.com
887 WWW::Bugzilla - Module providing API to create or update Bugzilla bugs.
888 Copyright (C) 2003 Matthew C. Vella (the_mcv@yahoo.com)
890 Portions Copyright (C) 2006 Brian Caswell (bmc@shmoo.com)
892 This module is free software; you can redistribute it and/or modify it
893 under the terms of either:
895 a) the GNU General Public License as published by the Free Software
896 Foundation; either version 1, or (at your option) any later version,
899 b) the "Artistic License" which comes with this module.
901 This program is distributed in the hope that it will be useful,
902 but WITHOUT ANY WARRANTY; without even the implied warranty of
903 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See either
904 the GNU General Public License or the Artistic License for more details.
906 You should have received a copy of the Artistic License with this
907 module, in the file ARTISTIC. If not, I'll be glad to provide one.
909 You should have received a copy of the GNU General Public License
910 along with this program; if not, write to the Free Software
911 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307