diff options
-rwxr-xr-x | checksetup.pl | 6 | ||||
-rwxr-xr-x | importxml.pl | 1741 | ||||
-rw-r--r-- | t/Support/Files.pm | 2 |
3 files changed, 1200 insertions, 549 deletions
diff --git a/checksetup.pl b/checksetup.pl index 56db99b46..847ada5bb 100755 --- a/checksetup.pl +++ b/checksetup.pl @@ -379,7 +379,7 @@ foreach my $module (@{$modules}) { print "\nThe following Perl modules are optional:\n" unless $silent; my $gd = have_vers("GD","1.20"); my $chartbase = have_vers("Chart::Base","1.0"); -my $xmlparser = have_vers("XML::Parser",0); +my $xmlparser = have_vers("XML::Twig",0); my $gdgraph = have_vers("GD::Graph",0); my $gdtextalign = have_vers("GD::Text::Align",0); my $patchreader = have_vers("PatchReader","0.9.4"); @@ -405,8 +405,8 @@ if ((!$gd || !$chartbase) && !$silent) { if (!$xmlparser && !$silent) { print "If you want to use the bug import/export feature to move bugs to\n", "or from other bugzilla installations, you will need to install\n ", - "the XML::Parser module by running (as $::root):\n\n", - " " . install_command("XML::Parser") . "\n\n"; + "the XML::Twig module by running (as $::root):\n\n", + " " . install_command("XML::Twig") . "\n\n"; } if (!$imagemagick && !$silent) { print "If you want to convert BMP image attachments to PNG to conserve\n", diff --git a/importxml.pl b/importxml.pl index 9c56b3d1a..e3a116921 100755 --- a/importxml.pl +++ b/importxml.pl @@ -1,4 +1,4 @@ -#!/usr/bin/perl -w +#!/usr/bin/perl -wT # -*- Mode: perl; indent-tabs-mode: nil -*- # # The contents of this file are subject to the Mozilla Public @@ -19,9 +19,10 @@ # Rights Reserved. # # Contributor(s): Dawn Endico <endico@mozilla.org> +# Gregary Hendricks <ghendricks@novell.com> +# Vance Baarda <vrb@novell.com> - -# This script reads in xml bug data from standard input and inserts +# This script reads in xml bug data from standard input and inserts # a new bug into bugzilla. Everything before the beginning <?xml line # is removed so you can pipe in email messages. @@ -29,19 +30,27 @@ use strict; ##################################################################### # -# This script is used import bugs from another installation of bugzilla. -# Moving a bug on another system will send mail to an alias provided by +# This script is used to import bugs from another installation of bugzilla. +# It can be used in two ways. +# First using the move function of bugzilla +# on another system will send mail to an alias provided by # the administrator of the target installation (you). Set up an alias -# similar to the one given below so this mail will be automatically +# similar to the one given below so this mail will be automatically # run by this script and imported into your database. Run 'newaliases' # after adding this alias to your aliases file. Make sure your sendmail -# installation is configured to allow mail aliases to execute code. +# installation is configured to allow mail aliases to execute code. # # bugzilla-import: "|/usr/bin/perl /opt/bugzilla/importxml.pl" # +# Second it can be run from the command line with any xml file from +# STDIN that conforms to the bugzilla DTD. In this case you can pass +# an argument to set whether you want to send the +# mail that will be sent to the exporter and maintainer normally. +# +# importxml.pl bugsfile.xml +# ##################################################################### - # figure out which path this script lives in. Set the current path to # this and add it to @INC so this will work when run as part of mail # alias by the mailer daemon @@ -49,583 +58,1225 @@ use strict; # $::path declaration in a BEGIN block so that it is executed before # the rest of the file is compiled. BEGIN { - $::path = $0; - $::path =~ m#(.*)/[^/]+#; - $::path = $1; - $::path ||= '.'; # $0 is empty at compile time. This line will - # have no effect on this script at runtime. + $::path = $0; + $::path =~ m#(.*)/[^/]+#; + $::path = $1; + $::path ||= '.'; # $0 is empty at compile time. This line will + # have no effect on this script at runtime. } chdir $::path; use lib ($::path); +# Data dumber is used for debugging, I got tired of copying it back in +# and then removing it. +#use Data::Dumper; + use Bugzilla; +use Bugzilla::Bug; +use Bugzilla::Product; +use Bugzilla::Version; +use Bugzilla::Component; +use Bugzilla::Milestone; +use Bugzilla::FlagType; use Bugzilla::Config qw(:DEFAULT $datadir); use Bugzilla::BugMail; - -use XML::Parser; -use Data::Dumper; -$Data::Dumper::Useqq = 1; -use Bugzilla::BugMail; use Bugzilla::User; +use Bugzilla::Util; +use Bugzilla::Constants; + +use MIME::Base64; +use MIME::Parser; +use Date::Format; +use Getopt::Long; +use Pod::Usage; +use XML::Twig; require "globals.pl"; +# We want to capture errors and handle them here rather than have the Template +# code barf all over the place. +Bugzilla->batch(1); + +my $debug = 0; +my $mail = ''; +my $help = 0; + +my $result = GetOptions( + "verbose|debug+" => \$debug, + "mail|sendmail!" => \$mail, + "help|?" => \$help +); + +pod2usage(0) if $help; + +use constant OK_LEVEL => 3; +use constant DEBUG_LEVEL => 2; +use constant ERR_LEVEL => 1; + GetVersionTable(); +our $log; +our @attachments; +our $bugtotal; +my $xml; +my $dbh = Bugzilla->dbh; +my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); + +############################################################################### +# Helper sub routines # +############################################################################### +# This can go away as soon as data/versioncache is removed. Since we still +# have to use GetVersionTable() though, it stays for now. sub sillyness { my $zz; - $zz = $Data::Dumper::Useqq; - $zz = %::versions; - $zz = %::keywordsbyname; $zz = @::legal_bug_status; $zz = @::legal_opsys; $zz = @::legal_platform; $zz = @::legal_priority; $zz = @::legal_severity; $zz = @::legal_resolution; - $zz = %::target_milestone; -} - -# XML::Parser automatically unquotes characters when it -# parses the XML, so this routine shouldn't be needed -# for anything (see bug 109530). -sub UnQuoteXMLChars { - $_[0] =~ s/&/&/g; - $_[0] =~ s/</</g; - $_[0] =~ s/>/>/g; - $_[0] =~ s/'/'/g; # ' # Darned emacs colors - $_[0] =~ s/"/"/g; # " # Darned emacs colors -# $_[0] =~ s/([\x80-\xFF])/&XmlUtf8Encode(ord($1))/ge; - return($_[0]); } sub MailMessage { - my $subject = shift @_; - my $message = shift @_; - my @recipients = @_; - - my $to = join (", ", @recipients); - my $header = "To: $to\n"; - my $from = Param("moved-from-address"); - $from =~ s/@/\@/g; - $header.= "From: Bugzilla <$from>\n"; - $header.= "Subject: $subject\n\n"; - - my $sendmessage = $header . $message . "\n"; - Bugzilla::BugMail::MessageToMTA($sendmessage); -} + return unless ($mail); + my $subject = shift; + my $message = shift; + my @recipients = @_; + my $from = Param("moved-from-address"); + $from =~ s/@/\@/g; + foreach my $to (@recipients){ + my $header = "To: $to\n"; + $header .= "From: Bugzilla <$from>\n"; + $header .= "Subject: $subject\n\n"; + my $sendmessage = $header . $message . "\n"; + Bugzilla::BugMail::MessageToMTA($sendmessage); + } -my $xml; -while (<>) { - $xml .= $_; } -# remove everything in file before xml header (i.e. remove the mail header) -$xml =~ s/^.+(<\?xml version.+)$/$1/s; -my $parser = new XML::Parser(Style => 'Tree'); -my $tree = $parser->parse($xml); -my $dbh = Bugzilla->dbh; +sub Debug { + return unless ($debug); + my ( $message, $level ) = (@_); + print STDERR "OK: $message \n" if ( $level == OK_LEVEL ); + print STDERR "ERR: $message \n" if ( $level == ERR_LEVEL ); + print STDERR "$message\n" + if ( ( $debug == $level ) && ( $level == DEBUG_LEVEL ) ); +} -my $maintainer; -if (defined $tree->[1][0]->{'maintainer'}) { - $maintainer= $tree->[1][0]->{'maintainer'}; -} else { - my $subject = "Bug import error: no maintainer"; - my $message = "Cannot import these bugs because no maintainer for "; - $message .= "the exporting db is given.\n"; - $message .= "\n\nPlease re-open the original bug.\n"; - $message .= "\n\n$xml"; - my @to = (Param("maintainer")); - MailMessage ($subject, $message, @to); - exit; +sub Error { + my ( $reason, $errtype, $exporter ) = @_; + my $subject = "Bug import error: $reason"; + my $message = "Cannot import these bugs because $reason "; + $message .= "\n\nPlease re-open the original bug.\n" if ($errtype); + $message .= "For more info, contact " . Param("maintainer") . ".\n"; + my @to = ( Param("maintainer"), $exporter); + Debug( $message, ERR_LEVEL ); + MailMessage( $subject, $message, @to ); + exit; } -my $exporter; -if (defined $tree->[1][0]->{'exporter'}) { - $exporter = $tree->[1][0]->{'exporter'}; -} else { - my $subject = "Bug import error: no exporter"; - my $message = "Cannot import these bugs because no exporter is given.\n"; - $message .= "\n\nPlease re-open the original bug.\n"; - $message .= "\n\n$xml"; - my @to = (Param("maintainer"), $maintainer); - MailMessage ($subject, $message, @to); - exit; +# This will be implemented in Bugzilla::Field as soon as bug 31506 lands +sub check_field { + my ($name, $value, $legalsRef, $no_warn) = @_; + my $dbh = Bugzilla->dbh; + + if (!defined($value) + || trim($value) eq "" + || (defined($legalsRef) && lsearch($legalsRef, $value) < 0)) + { + return 0 if $no_warn; # We don't want an error to be thrown; return. + + trick_taint($name); + my ($result) = $dbh->selectrow_array("SELECT description FROM fielddefs + WHERE name = ?", undef, $name); + + my $field = $result || $name; + ThrowCodeError('illegal_field', { field => $field }); + } + return 1; } +# This subroutine handles flags for process_bug. It is generic in that +# it can handle both attachment flags and bug flags. +sub flag_handler { + my ( + $name, $status, $setter_login, + $requestee_login, $exporterid, $bugid, + $productid, $componentid, $attachid + ) + = @_; + + my $type = ($attachid) ? "attachment" : "bug"; + my $err = ''; + my $setter = Bugzilla::User->new_from_login($setter_login); + my $requestee; + my $requestee_id; + + unless ($setter) { + $err = "Invalid setter $setter_login on $type flag $name\n"; + $err .= " Dropping flag $name\n"; + return $err; + } + if ( !$setter->can_see_bug($bugid) ) { + $err .= "Setter is not a member of bug group\n"; + $err .= " Dropping flag $name\n"; + return $err; + } + my $setter_id = $setter->id; + if ( defined($requestee_login) ) { + $requestee = Bugzilla::User->new_from_login($requestee_login); + if ( $requestee ) { + if ( !$requestee->can_see_bug($bugid) ) { + $err .= "Requestee is not a member of bug group\n"; + $err .= " Requesting from the wind\n"; + } + else{ + $requestee_id = $requestee->id; + } + } + else { + $err = "Invalid requestee $requestee_login on $type flag $name\n"; + $err .= " Requesting from the wind.\n"; + } + + } + my $flag_types; + + # If this is an attachment flag we need to do some dirty work to look + # up the flagtype ID + if ($attachid) { + $flag_types = Bugzilla::FlagType::match( + { + 'target_type' => 'attachment', + 'product_id' => $productid, + 'component_id' => $componentid + } ); + } + else { + my $bug = new Bugzilla::Bug( $bugid, $exporterid ); + $flag_types = $bug->flag_types; + } + unless ($flag_types){ + $err = "No flag types defined for this bug\n"; + $err .= " Dropping flag $name\n"; + return $err; + } + + # We need to see if the imported flag is in the list of known flags + # It is possible for two flags on the same bug have the same name + # If this is the case, we will only match the first one. + my $ftype; + foreach my $f ( @{$flag_types} ) { + if ( $f->{'name'} eq $name) { + $ftype = $f; + last; + } + } + + if ($ftype) { # We found the flag in the list + my $grant_gid = $ftype->{'grant_gid'}; + if (( $status eq '+' || $status eq '-' ) + && $grant_gid && !$setter->in_group_id($grant_gid)) { + $err = "Setter $setter_login on $type flag $name "; + $err .= "is not in the Grant Group\n"; + $err .= " Dropping flag $name\n"; + return $err; + } + my $request_gid = $ftype->{'request_gid'}; + if ($request_gid + && $status eq '?' && !$setter->in_group_id($request_gid)) { + $err = "Setter $setter_login on $type flag $name "; + $err .= "is not in the Request Group\n"; + $err .= " Dropping flag $name\n"; + return $err; + } -unless ( Param("move-enabled") ) { - my $subject = "Error: bug importing is disabled here"; - my $message = "Cannot import these bugs because importing is disabled\n"; - $message .= "at this site. For more info, contact "; - $message .= Param("maintainer") . ".\n"; - my @to = (Param("maintainer"), $maintainer, $exporter); - MailMessage ($subject, $message, @to); - exit; + # Take the first flag_type that matches + my $ftypeid = $ftype->{'id'}; + my $is_active = $ftype->{'is_active'}; + unless ($is_active) { + $err = "Flag $name is not active in this database\n"; + $err .= " Dropping flag $name\n"; + return $err; + } + + my ($fid) = $dbh->selectrow_array("SELECT MAX(id) FROM flags") || 0; + $dbh->do("INSERT INTO flags + (id, type_id, status, bug_id, attach_id, creation_date, + setter_id, requestee_id, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", undef, + ++$fid, $ftypeid, $status, $bugid, $attachid, $timestamp, + $setter_id, $requestee_id, 1 + ); + } + else { + $err = "Dropping unknown $type flag: $name\n"; + return $err; + } + return $err; } -my $exporterid = login_to_id($exporter); -if ( ! $exporterid ) { - my $subject = "Bug import error: invalid exporter"; - my $message = "The user <$tree->[1][0]->{'exporter'}> who tried to move\n"; - $message .= "bugs here does not have an account in this database.\n"; - $message .= "\n\nPlease re-open the original bug.\n"; - $message .= "\n\n$xml"; - my @to = (Param("maintainer"), $maintainer, $exporter); - MailMessage ($subject, $message, @to); - exit; +############################################################################### +# XML Handlers # +############################################################################### + +# This subroutine gets called only once - as soon as the <bugzilla> opening +# tag is parsed. It simply checks to see that the all important exporter +# maintainer and URL base are set. +# +# exporter: email address of the person moving the bugs +# maintainer: the maintainer of the bugzilla installation +# as set in the parameters file +# urlbase: The urlbase paramter of the installation +# bugs are being moved from +# +sub init() { + my ( $twig, $bugzilla ) = @_; + my $root = $twig->root; + my $maintainer = $root->{'att'}->{'maintainer'}; + my $exporter = $root->{'att'}->{'exporter'}; + my $urlbase = $root->{'att'}->{'urlbase'}; + my $xmlversion = $root->{'att'}->{'version'}; + + if ($xmlversion ne $Bugzilla::Config::VERSION) { + $log .= "Possible version conflict!\n"; + $log .= " XML was exported from Bugzilla version $xmlversion\n"; + $log .= " But this installation uses "; + $log .= $Bugzilla::Config::VERSION . "\n"; + } + Error( "no maintainer", "REOPEN", $exporter ) unless ($maintainer); + Error( "no exporter", "REOPEN", $exporter ) unless ($exporter); + Error( "bug importing is disabled here", undef, $exporter ) unless ( Param("move-enabled") ); + Error( "invalid exporter: $exporter", "REOPEN", $exporter ) if ( !login_to_id($exporter) ); + Error( "no urlbase set", "REOPEN", $exporter ) unless ($urlbase); + my $def_product = + new Bugzilla::Product( { name => Param("moved-default-product") } ) + || Error("Cannot import these bugs because an invalid default + product was defined for the target db." + . Param("maintainer") . " needs to fix the definitions of + moved-default-product. \n", "REOPEN", $exporter); + my $def_component = new Bugzilla::Component( + { + product_id => $def_product->id, + name => Param("moved-default-component") + }) + || Error("Cannot import these bugs because an invalid default + component was defined for the target db." + . Param("maintainer") . " needs to fix the definitions of + moved-default-component.\n", "REOPEN", $exporter); } + -my $urlbase; -if (defined $tree->[1][0]->{'urlbase'}) { - $urlbase= $tree->[1][0]->{'urlbase'}; -} else { - my $subject = "Bug import error: invalid exporting database"; - my $message = "Cannot import these bugs because the name of the exporting db was not given.\n"; - $message .= "\n\nPlease re-open the original bug.\n"; - $message .= "\n\n$xml"; - my @to = (Param("maintainer"), $maintainer, $exporter); - MailMessage ($subject, $message, @to); - exit; +# Parse attachments. +# +# This subroutine is called once for each attachment in the xml file. +# It is called as soon as the closing </attachment> tag is parsed. +# Since attachments have the potential to be very large, and +# since each attachement will be inside <bug>..</bug> tags we shove +# the attachment onto an array which will be processed by process_bug +# and then disposed of. The attachment array will then contain only +# one bugs' attachments at a time. +# The cycle will then repeat for the next <bug> +# +# The attach_id is ignored since mysql generates a new one for us. +# The submitter_id gets filled in with $exporterid. + +sub process_attachment() { + my ( $twig, $attach ) = @_; + Debug( "Parsing attachments", DEBUG_LEVEL ); + my %attachment; + + $attachment{'date'} = + format_time( $attach->field('date'), "%Y-%m-%d %R" ) || $timestamp; + $attachment{'desc'} = $attach->field('desc'); + $attachment{'ctype'} = $attach->field('type') || "unknown/unknown"; + $attachment{'attachid'} = $attach->field('attachid'); + $attachment{'ispatch'} = $attach->{'att'}->{'ispatch'} || 0; + $attachment{'isobsolete'} = $attach->{'att'}->{'isobsolete'} || 0; + $attachment{'isprivate'} = $attach->{'att'}->{'isprivate'} || 0; + $attachment{'filename'} = $attach->field('filename') || "file"; + if ( defined( $attach->first_child('data')->{'att'}->{'encoding'} ) + && $attach->first_child('data')->{'att'}->{'encoding'} =~ /base64/ ) + { + # decode the base64 + my $data = $attach->field('data'); + my $output = decode_base64($data); + $attachment{'data'} = $output; + } + else { + $attachment{'data'} = $attach->field('data'); + } + + # attachment flags + my @aflags; + foreach my $aflag ( $attach->children('flag') ) { + my %aflag; + $aflag{'name'} = $aflag->{'att'}->{'name'}; + $aflag{'status'} = $aflag->{'att'}->{'status'}; + $aflag{'setter'} = $aflag->{'att'}->{'setter'}; + $aflag{'requestee'} = $aflag->{'att'}->{'requestee'}; + push @aflags, \%aflag; + } + $attachment{'flags'} = \@aflags if (@aflags); + + # free up the memory for use by the rest of the script + $attach->delete; + if ($attachment{'attachid'}) { + push @attachments, \%attachment; + } + else { + push @attachments, "err"; + } } - - -my $bugqty = ($#{$tree->[1]} +1 -3) / 4; -my $log = "Imported $bugqty bug(s) from $urlbase,\n sent by $exporter.\n\n"; -for (my $k=1 ; $k <= $bugqty ; $k++) { - my $cur = $k*4; - - if (defined $tree->[1][$cur][0]->{'error'}) { - $log .= "\nError in bug $tree->[1][$cur][4][2]\@$urlbase:"; - $log .= " $tree->[1][$cur][0]->{'error'}\n"; - if ($tree->[1][$cur][0]->{'error'} =~ /NotFound/) { - $log .= "$exporter tried to move bug $tree->[1][$cur][4][2] here"; - $log .= " but $urlbase reports that this bug does not exist.\n"; - } elsif ( $tree->[1][$cur][0]->{'error'} =~ /NotPermitted/) { - $log .= "$exporter tried to move bug $tree->[1][$cur][4][2] here"; - $log .= " but $urlbase reports that $exporter does not have access"; - $log .= " to that bug.\n"; - } - next; - } - - my %multiple_fields; - foreach my $field (qw (dependson cc long_desc blocks)) { - $multiple_fields{$field} = "x"; - } - my %all_fields; - foreach my $field (qw (dependson product bug_status priority cc version - bug_id rep_platform short_desc assigned_to bug_file_loc resolution - delta_ts component reporter urlbase target_milestone bug_severity - creation_ts qa_contact keywords status_whiteboard op_sys blocks)) { - $all_fields{$field} = "x"; - } - - - my %bug_fields; - my $err = ""; - for (my $i=3 ; $i < $#{$tree->[1][$cur]} ; $i=$i+4) { - if (defined $multiple_fields{$tree->[1][$cur][$i]}) { - if (defined $bug_fields{$tree->[1][$cur][$i]}) { - $bug_fields{$tree->[1][$cur][$i]} .= " " . $tree->[1][$cur][$i+1][2]; - } else { - $bug_fields{$tree->[1][$cur][$i]} = $tree->[1][$cur][$i+1][2]; - } - } elsif (defined $all_fields{$tree->[1][$cur][$i]}) { - $bug_fields{$tree->[1][$cur][$i]} = $tree->[1][$cur][$i+1][2]; - } else { - $err .= "---\n"; - $err .= "Unknown bug field \"$tree->[1][$cur][$i]\""; - $err .= " encountered while moving bug\n"; - $err .= "<$tree->[1][$cur][$i]>"; - if (defined $tree->[1][$cur][$i+1][3]) { - $err .= "\n"; - for (my $j=3 ; $j < $#{$tree->[1][$cur][$i+1]} ; $j=$j+4) { - $err .= " <". $tree->[1][$cur][$i+1][$j] . ">"; - $err .= " $tree->[1][$cur][$i+1][$j+1][2] "; - $err .= "</". $tree->[1][$cur][$i+1][$j] . ">\n"; - } - } else { - $err .= " $tree->[1][$cur][$i+1][2] "; - } - $err .= "</$tree->[1][$cur][$i]>\n"; - } - } - - my @long_descs; - for (my $i=3 ; $i < $#{$tree->[1][$cur]} ; $i=$i+4) { - if ($tree->[1][$cur][$i] =~ /long_desc/) { - my %long_desc; - $long_desc{'who'} = $tree->[1][$cur][$i+1][4][2]; - $long_desc{'bug_when'} = $tree->[1][$cur][$i+1][8][2]; - $long_desc{'thetext'} = $tree->[1][$cur][$i+1][12][2]; - push @long_descs, \%long_desc; - } - } - - # instead of giving each comment its own item in the longdescs - # table like it should have, lets cat them all into one big - # comment otherwise we would have to lie often about who - # authored the comment since commenters in one bugzilla probably - # don't have accounts in the other one. - sub by_date {my @a; my @b; $a->{'bug_when'} cmp $b->{'bug_when'}; } - my @sorted_descs = sort by_date @long_descs; - my $long_description = ""; - for (my $z=0 ; $z <= $#sorted_descs ; $z++) { - unless ( $z==0 ) { - $long_description .= "\n\n\n------- Additional Comments From "; - $long_description .= "$sorted_descs[$z]->{'who'} "; - $long_description .= "$sorted_descs[$z]->{'bug_when'}"; - $long_description .= " ----\n\n"; - } - $long_description .= $sorted_descs[$z]->{'thetext'}; - $long_description .= "\n"; - } - - my $comments; - - $comments .= "\n\n------- Bug moved to this database by $exporter "; - $comments .= time2str("%Y-%m-%d %H:%M", time); - $comments .= " -------\n\n"; - $comments .= "This bug previously known as bug $bug_fields{'bug_id'} at "; - $comments .= $urlbase . "\n"; - $comments .= $urlbase . "show_bug.cgi?"; - $comments .= "id=" . $bug_fields{'bug_id'} . "\n"; - $comments .= "Originally filed under the $bug_fields{'product'} "; - $comments .= "product and $bug_fields{'component'} component.\n"; - if (defined $bug_fields{'dependson'}) { - $comments .= "Bug depends on bug(s) $bug_fields{'dependson'}.\n"; - } - if (defined $bug_fields{'blocks'}) { - $comments .= "Bug blocks bug(s) $bug_fields{'blocks'}.\n"; - } - - my @query = (); - my @values = (); - foreach my $field ( qw(creation_ts status_whiteboard) ) { - if ( (defined $bug_fields{$field}) && ($bug_fields{$field}) ){ - push (@query, "$field"); - push (@values, SqlQuote($bug_fields{$field})); - } - } - - push (@query, "delta_ts"); - if ( (defined $bug_fields{'delta_ts'}) && ($bug_fields{'delta_ts'}) ){ - push (@values, SqlQuote($bug_fields{'delta_ts'})); - } - else { - push (@values, "NOW()"); - } - - if ( (defined $bug_fields{'bug_file_loc'}) && ($bug_fields{'bug_file_loc'}) ){ - push (@query, "bug_file_loc"); - push (@values, SqlQuote($bug_fields{'bug_file_loc'})); - } - - if ( (defined $bug_fields{'short_desc'}) && ($bug_fields{'short_desc'}) ){ - push (@query, "short_desc"); - push (@values, SqlQuote($bug_fields{'short_desc'}) ); - } - - - my $prod; - my $comp; - my $default_prod = Param("moved-default-product"); - my $default_comp = Param("moved-default-component"); - if ( (defined ($bug_fields{'product'})) && - (defined ($bug_fields{'component'})) ) { - $prod = $bug_fields{'product'}; - $comp = $bug_fields{'component'}; - } else { - $prod = $default_prod; - $comp = $default_comp; - } - - # XXX - why are these arrays?? - my @product; - my @component; - my $prod_id; - my $comp_id; - - # First, try the given product/component - $prod_id = get_product_id($prod); - $comp_id = get_component_id($prod_id, $comp) if $prod_id; - - if ($prod_id && $comp_id) { - $product[0] = $prod; - $component[0] = $comp; - } else { - # Second, try the defaults - $prod_id = get_product_id($default_prod); - $comp_id = get_component_id($prod_id, $default_comp) if $prod_id; - if ($prod_id && $comp_id) { - $product[0] = $default_prod; - $component[0] = $default_comp; - } - } - - if ($prod_id && $comp_id) { - push (@query, "product_id"); - push (@values, $prod_id ); - push (@query, "component_id"); - push (@values, $comp_id ); - } else { - my $subject = "Bug import error: invalid default product or component"; - my $message = "Cannot import these bugs because an invalid default "; - $message .= "product and/or component was defined for the target db.\n"; - $message .= Param("maintainer") . " needs to fix the definitions of "; - $message .= "moved-default-product and moved-default-component.\n"; - $message .= "\n\nPlease re-open the original bug.\n"; - $message .= "\n\n$xml"; - my @to = (Param("maintainer"), $maintainer, $exporter); - MailMessage ($subject, $message, @to); - exit; - } - - if (defined ($::versions{$product[0]} ) && - (my @version = grep(lc($_) eq lc($bug_fields{'version'}), - @{$::versions{$product[0]}})) ){ - push (@values, SqlQuote($version[0]) ); - push (@query, "version"); - } else { - push (@query, "version"); - push (@values, SqlQuote($::versions{$product[0]}->[0])); - $err .= "Unknown version $bug_fields{'version'} in product $product[0]. "; - $err .= "Setting version to \"$::versions{$product[0]}->[0]\".\n"; - } - - if (defined ($bug_fields{'priority'}) && - (my @priority = grep(lc($_) eq lc($bug_fields{'priority'}), @::legal_priority)) ){ - push (@values, SqlQuote($priority[0]) ); - push (@query, "priority"); - } else { - push (@values, SqlQuote("P3")); - push (@query, "priority"); - $err .= "Unknown priority "; - $err .= (defined $bug_fields{'priority'})?$bug_fields{'priority'}:"unknown"; - $err .= ". Setting to default priority \"P3\".\n"; - } - - if (defined ($bug_fields{'rep_platform'}) && - (my @platform = grep(lc($_) eq lc($bug_fields{'rep_platform'}), @::legal_platform)) ){ - push (@values, SqlQuote($platform[0]) ); - push (@query, "rep_platform"); - } else { - push (@values, SqlQuote("Other") ); - push (@query, "rep_platform"); - $err .= "Unknown platform "; - $err .= (defined $bug_fields{'rep_platform'})? - $bug_fields{'rep_platform'}:"unknown"; - $err .= ". Setting to default platform \"Other\".\n"; - } - - if (defined ($bug_fields{'op_sys'}) && - (my @opsys = grep(lc($_) eq lc($bug_fields{'op_sys'}), @::legal_opsys)) ){ - push (@values, SqlQuote($opsys[0]) ); - push (@query, "op_sys"); - } else { - push (@values, SqlQuote("other")); - push (@query, "op_sys"); - $err .= "Unknown operating system "; - $err .= (defined $bug_fields{'op_sys'})?$bug_fields{'op_sys'}:"unknown"; - $err .= ". Setting to default OS \"other\".\n"; - } - - if (Param("usetargetmilestone")) { - if (defined ($::target_milestone{$product[0]} ) && - (my @tm = grep(lc($_) eq lc($bug_fields{'target_milestone'}), - @{$::target_milestone{$product[0]}})) ){ - push (@values, SqlQuote($tm[0]) ); - push (@query, "target_milestone"); - } else { - SendSQL("SELECT defaultmilestone FROM products " . - "WHERE name = " . SqlQuote($product[0])); - my $tm = FetchOneColumn(); - push (@values, SqlQuote($tm)); - push (@query, "target_milestone"); - $err .= "Unknown milestone \""; - $err .= (defined $bug_fields{'target_milestone'})? - $bug_fields{'target_milestone'}:"unknown"; - $err .= "\" in product \"$product[0]\".\n"; - $err .= " Setting to default milestone for this product, "; - $err .= "\'" . $tm . "\'\n"; - } - } - - if (defined ($bug_fields{'bug_severity'}) && - (my @severity= grep(lc($_) eq lc($bug_fields{'bug_severity'}), - @::legal_severity)) ){ - push (@values, SqlQuote($severity[0]) ); - push (@query, "bug_severity"); - } else { - push (@values, SqlQuote("normal")); - push (@query, "bug_severity"); - $err .= "Unknown severity "; - $err .= (defined $bug_fields{'bug_severity'})? - $bug_fields{'bug_severity'}:"unknown"; - $err .= ". Setting to default severity \"normal\".\n"; - } - - my $reporterid = login_to_id($bug_fields{'reporter'}); - if ( ($bug_fields{'reporter'}) && ( $reporterid ) ) { - push (@values, SqlQuote($reporterid)); - push (@query, "reporter"); - } else { - push (@values, SqlQuote($exporterid)); - push (@query, "reporter"); - $err .= "The original reporter of this bug does not have\n"; - $err .= " an account here. Reassigning to the person who moved\n"; - $err .= " it here, $exporter.\n"; - if ( $bug_fields{'reporter'} ) { - $err .= " Previous reporter was $bug_fields{'reporter'}.\n"; - } else { - $err .= " Previous reporter is unknown.\n"; - } - } - - my $changed_owner = 0; - if ( ($bug_fields{'assigned_to'}) && - ( login_to_id($bug_fields{'assigned_to'})) ) { - push (@values, SqlQuote(login_to_id($bug_fields{'assigned_to'}))); - push (@query, "assigned_to"); - } else { - push (@values, SqlQuote($exporterid) ); - push (@query, "assigned_to"); - $changed_owner = 1; - $err .= "The original assignee of this bug does not have\n"; - $err .= " an account here. Reassigning to the person who moved\n"; - $err .= " it here, $exporter.\n"; - if ( $bug_fields{'assigned_to'} ) { - $err .= " Previous assignee was $bug_fields{'assigned_to'}.\n"; - } else { - $err .= " Previous assignee is unknown.\n"; - } - } - - my @resolution; - if (defined ($bug_fields{'resolution'}) && - (@resolution= grep(lc($_) eq lc($bug_fields{'resolution'}), @::legal_resolution)) ){ - push (@values, SqlQuote($resolution[0]) ); - push (@query, "resolution"); - } elsif ( (defined $bug_fields{'resolution'}) && (!$resolution[0]) ){ - $err .= "Unknown resolution \"$bug_fields{'resolution'}\".\n"; - } - - # if the bug's assignee changed, mark the bug NEW, unless a valid - # resolution is set, which indicates that the bug should be closed. - # - if ( ($changed_owner) && (!$resolution[0]) ) { - push (@values, SqlQuote("NEW")); - push (@query, "bug_status"); - $err .= "Bug reassigned, setting status to \"NEW\".\n"; - $err .= " Previous status was \""; - $err .= (defined $bug_fields{'bug_status'})? - $bug_fields{'bug_status'}:"unknown"; - $err .= "\".\n"; - } elsif ( (defined ($bug_fields{'resolution'})) && (!$resolution[0]) ){ - #if the resolution was illegal then set status to NEW - push (@values, SqlQuote("NEW")); - push (@query, "bug_status"); - $err .= "Resolution was invalid. Setting status to \"NEW\".\n"; - $err .= " Previous status was \""; - $err .= (defined $bug_fields{'bug_status'})? - $bug_fields{'bug_status'}:"unknown"; - $err .= "\".\n"; - } elsif (defined ($bug_fields{'bug_status'}) && - (my @status = grep(lc($_) eq lc($bug_fields{'bug_status'}), @::legal_bug_status)) ){ - #if a bug status was set then use it, if its legal - push (@values, SqlQuote($status[0])); - push (@query, "bug_status"); - } else { - # if all else fails, make the bug new - push (@values, SqlQuote("NEW")); - push (@query, "bug_status"); - $err .= "Unknown status "; - $err .= (defined $bug_fields{'bug_status'})? - $bug_fields{'bug_status'}:"unknown"; - $err .= ". Setting to default status \"NEW\".\n"; - } - - if (Param("useqacontact")) { - my $qa_contact; - if ( (defined $bug_fields{'qa_contact'}) && - ($qa_contact = login_to_id($bug_fields{'qa_contact'})) ){ - push (@values, $qa_contact); - push (@query, "qa_contact"); - } else { - SendSQL("SELECT initialqacontact FROM components, products " . - "WHERE components.product_id = products.id" . - " AND products.name = " . SqlQuote($product[0]) . - " AND components.name = " . SqlQuote($component[0]) ); - $qa_contact = FetchOneColumn(); - push (@values, $qa_contact); - push (@query, "qa_contact"); - $err .= "Setting qa contact to the default for this product.\n"; - $err .= " This bug either had no qa contact or an invalid one.\n"; - } - } - - - my $query = "INSERT INTO bugs (\n" - . join (",\n", @query) - . "\n) VALUES (\n" - . join (",\n", @values) - . "\n)\n"; - SendSQL($query); - my $id = $dbh->bz_last_key('bugs', 'bug_id'); - - if (defined $bug_fields{'cc'}) { - foreach my $person (split(/[ ,]/, $bug_fields{'cc'})) { - my $uid; - if ( ($person ne "") && ($uid = login_to_id($person)) ) { - SendSQL("insert into cc (bug_id, who) values ($id, " . SqlQuote($uid) .")"); - } - } - } - - if (defined ($bug_fields{'keywords'})) { - my %keywordseen; - foreach my $keyword (split(/[\s,]+/, $bug_fields{'keywords'})) { - if ($keyword eq '') { - next; - } - my $i = $::keywordsbyname{$keyword}; - if (!$i) { - $err .= "Skipping unknown keyword: $keyword.\n"; - next; - } - if (!$keywordseen{$i}) { - SendSQL("INSERT INTO keywords (bug_id, keywordid) VALUES ($id, $i)"); - $keywordseen{$i} = 1; - } - } - } - - $long_description .= "\n" . $comments; - if ($err) { - $long_description .= "\n$err\n"; - } - - SendSQL("INSERT INTO longdescs (bug_id, who, bug_when, thetext) VALUES " . - "($id, $exporterid, now(), " . SqlQuote($long_description) . ")"); - - $log .= "Bug $urlbase/show_bug.cgi?id=$bug_fields{'bug_id'} "; - $log .= "imported as bug $id.\n"; - $log .= Param("urlbase") . "/show_bug.cgi?id=$id\n\n"; - if ($err) { - $log .= "The following problems were encountered creating bug $id.\n"; - $log .= "You may have to set certain fields in the new bug by hand.\n\n"; - $log .= $err; - $log .= "\n\n\n"; - } - - Bugzilla::BugMail::Send($id, { 'changer' => $exporter }); + +# This subroutine will be called once for each <bug> in the xml file. +# It is called as soon as the closing </bug> tag is parsed. +# If this bug had any <attachment> tags, they will have been processed +# before we get to this point and their data will be in the @attachments +# array. +# As each bug is processed, it is inserted into the database and then +# purged from memory to free it up for later bugs. + +sub process_bug { + my ( $twig, $bug ) = @_; + my $root = $twig->root; + my $maintainer = $root->{'att'}->{'maintainer'}; + my $exporter_login = $root->{'att'}->{'exporter'}; + my $exporter = Bugzilla::User->new_from_login($exporter_login); + my $urlbase = $root->{'att'}->{'urlbase'}; + + if ( defined $bug->{'att'}->{'error'} ) { + $log .= "\nError in bug " . $bug->field('bug_id') . "\@$urlbase: "; + $log .= $bug->{'att'}->{'error'} . "\n"; + if ( $bug->{'att'}->{'error'} =~ /NotFound/ ) { + $log .= "$exporter_login tried to move bug " . $bug->field('bug_id'); + $log .= " here, but $urlbase reports that this bug"; + $log .= " does not exist.\n"; + } + elsif ( $bug->{'att'}->{'error'} =~ /NotPermitted/ ) { + $log .= "$exporter_login tried to move bug " . $bug->field('bug_id'); + $log .= " here, but $urlbase reports that $exporter_login does "; + $log .= " not have access to that bug.\n"; + } + return; + } + $bugtotal++; + + # This list contains all other bug fields that we want to process. + # If it is not in this list it will not be included. + my %all_fields; + foreach my $field ( + qw(long_desc attachment flag group), Bugzilla::Bug::fields() ) + { + $all_fields{$field} = 1; + } + + my %bug_fields; + my $err = ""; + + # Loop through all the xml tags inside a <bug> and compare them to the + # lists of fields. If they match throw them into the hash. Otherwise + # append it to the log, which will go into the comments when we are done. + foreach my $bugchild ( $bug->children() ) { + Debug( "Parsing field: " . $bugchild->name, DEBUG_LEVEL ); + if ( defined $all_fields{ $bugchild->name } ) { + $bug_fields{ $bugchild->name } = + join( " ", $bug->children_text( $bugchild->name ) ); + } + else { + $err .= "Unknown bug field \"" . $bugchild->name . "\""; + $err .= " encountered while moving bug\n"; + $err .= " <" . $bugchild->name . ">"; + if ( $bugchild->children_count > 1 ) { + $err .= "\n"; + foreach my $subchild ( $bugchild->children() ) { + $err .= " <" . $subchild->name . ">"; + $err .= $subchild->field; + $err .= "</" . $subchild->name . ">\n"; + } + } + else { + $err .= $bugchild->field; + } + $err .= "</" . $bugchild->name . ">\n"; + } + } + + my @long_descs; + my $private = 0; + + # Parse long descriptions + foreach my $comment ( $bug->children('long_desc') ) { + Debug( "Parsing Long Description", DEBUG_LEVEL ); + my %long_desc; + $long_desc{'who'} = $comment->field('who'); + $long_desc{'bug_when'} = $comment->field('bug_when'); + $long_desc{'isprivate'} = $comment->{'att'}->{'isprivate'} || 0; + + # if one of the comments is private we need to set this flag + if ( $long_desc{'isprivate'} && $exporter->in_group(Param('insidergroup'))) { + $private = 1; + } + my $data = $comment->field('thetext'); + if ( defined $comment->first_child('thetext')->{'att'}->{'encoding'} + && $comment->first_child('thetext')->{'att'}->{'encoding'} =~ + /base64/ ) + { + $data = decode_base64($data); + } + + # If we leave the attachemnt ID in the comment it will be made a link + # to the wrong attachment. Since the new attachment ID is unkown yet + # let's strip it out for now. We will make a comment with the right ID + # later + $data =~ s/Created an attachment \(id=\d+\)/Created an attachment/g; + + # Same goes for bug #'s Since we don't know if the referenced bug + # is also being moved, lets make sure they know it means a different + # bugzilla. + my $url = $urlbase . "show_bug.cgi?id="; + $data =~ s/([Bb]ugs?\s*\#?\s*(\d+))/$url$2/g; + + $long_desc{'thetext'} = $data; + push @long_descs, \%long_desc; + } + + # instead of giving each comment its own item in the longdescs + # table like it should have, lets cat them all into one big + # comment otherwise we would have to lie often about who + # authored the comment since commenters in one bugzilla probably + # don't have accounts in the other one. + # If one of the comments is private the whole comment will be + # private since we don't want to expose these unnecessarily + sub by_date { my @a; my @b; $a->{'bug_when'} cmp $b->{'bug_when'}; } + my @sorted_descs = sort by_date @long_descs; + my $long_description = ""; + for ( my $z = 0 ; $z <= $#sorted_descs ; $z++ ) { + if ( $z == 0 ) { + $long_description .= "\n\n\n---- Reported by "; + } + else { + $long_description .= "\n\n\n---- Additional Comments From "; + } + $long_description .= "$sorted_descs[$z]->{'who'} "; + $long_description .= "$sorted_descs[$z]->{'bug_when'}"; + $long_description .= " ----"; + $long_description .= "\n\n"; + $long_description .= "THIS COMMENT IS PRIVATE \n" + if ( $sorted_descs[$z]->{'isprivate'} ); + $long_description .= $sorted_descs[$z]->{'thetext'}; + $long_description .= "\n"; + } + + my $comments; + + $comments .= "\n\n--- Bug imported by $exporter_login "; + $comments .= time2str( "%Y-%m-%d %H:%M", time ) . " "; + $comments .= Param('timezone'); + $comments .= " ---\n\n"; + $comments .= "This bug was previously known as _bug_ $bug_fields{'bug_id'} at "; + $comments .= $urlbase . "show_bug.cgi?id=" . $bug_fields{'bug_id'} . "\n"; + if ( defined $bug_fields{'dependson'} ) { + $comments .= "This bug depended on bug(s) $bug_fields{'dependson'}.\n"; + } + if ( defined $bug_fields{'blocked'} ) { + $comments .= "This bug blocked bug(s) $bug_fields{'blocked'}.\n"; + } + + # Now we process each of the fields in turn and make sure they contain + # valid data. We will create two parallel arrays, one for the query + # and one for the values. For every field we need to push an entry onto + # each array. + my @query = (); + my @values = (); + + # Each of these fields we will check for newlines and shove onto the array + foreach my $field (qw(status_whiteboard bug_file_loc short_desc)) { + if (( defined $bug_fields{$field} ) && ( $bug_fields{$field} )) { + $bug_fields{$field} = clean_text( $bug_fields{$field} ); + push( @query, $field ); + push( @values, $bug_fields{$field} ); + } + } + + # Alias + if ( $bug_fields{'alias'} ) { + my ($alias) = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs + WHERE alias = ?", undef, + $bug_fields{'alias'} ); + if ($alias) { + $err .= "Dropping conflicting bug alias "; + $err .= $bug_fields{'alias'} . "\n"; + } + else { + $alias = $bug_fields{'alias'}; + push @query, 'alias'; + push @values, $alias; + } + } + + # Timestamps + push( @query, "creation_ts" ); + push( @values, + format_time( $bug_fields{'creation_ts'}, "%Y-%m-%d %X" ) + || $timestamp ); + + push( @query, "delta_ts" ); + push( @values, + format_time( $bug_fields{'delta_ts'}, "%Y-%m-%d %X" ) + || $timestamp ); + + # Bug Access + push( @query, "cclist_accessible" ); + push( @values, $bug_fields{'cclist_accessible'} == 1 ? 1 : 0 ); + + push( @query, "reporter_accessible" ); + push( @values, $bug_fields{'reporter_accessible'} == 1 ? 1 : 0 ); + + # Product and Component if there is no valid default product and + # component defined in the parameters, we wouldn't be here + my $def_product = + new Bugzilla::Product( { name => Param("moved-default-product") } ); + my $def_component = new Bugzilla::Component( + { + product_id => $def_product->id, + name => Param("moved-default-component") + } + ); + my $product; + my $component; + + if ( defined $bug_fields{'product'} ) { + $product = new Bugzilla::Product( { name => $bug_fields{'product'} } ); + unless ($product) { + $product = $def_product; + $err .= "Unknown Product " . $bug_fields{'product'} . "\n"; + $err .= " Using default product set in Parameters \n"; + } + } + else { + $product = $def_product; + } + if ( defined $bug_fields{'component'} ) { + $component = new Bugzilla::Component( + { + product_id => $product->id, + name => $bug_fields{'component'} + } + ); + unless ($component) { + $component = $def_component; + $product = $def_product; + $err .= "Unknown Component " . $bug_fields{'component'} . "\n"; + $err .= " Using default product and component set "; + $err .= "in Parameters \n"; + } + } + else { + $component = $def_component; + $product = $def_product; + } + + my $prod_id = $product->id; + my $comp_id = $component->id; + + push( @query, "product_id" ); + push( @values, $prod_id ); + push( @query, "component_id" ); + push( @values, $comp_id ); + + # Since there is no default version for a product, we check that the one + # coming over is valid. If not we will use the first one in @versions + # and warn them. + my $version = + new Bugzilla::Version( $product->id, $bug_fields{'version'} ); + + push( @query, "version" ); + if ($version) { + push( @values, $version->name ); + } + else { + my @versions = @{ $product->versions }; + my $v = $versions[0]; + push( @values, $v->name ); + $err .= "Unknown version \""; + $err .= ( defined $bug_fields{'version'} ) + ? $bug_fields{'version'} + : "unknown"; + $err .= " in product " . $product->name . ". \n"; + $err .= " Setting version to \"" . $v->name . "\".\n"; + } + + # Milestone + if ( Param("usetargetmilestone") ) { + my $milestone = + new Bugzilla::Milestone( $product->id, + $bug_fields{'target_milestone'} ); + if ($milestone) { + push( @values, $milestone->name ); + } + else { + push( @values, $product->default_milestone ); + $err .= "Unknown milestone \""; + $err .= ( defined $bug_fields{'target_milestone'} ) + ? $bug_fields{'target_milestone'} + : "unknown"; + $err .= " in product " . $product->name . ". \n"; + $err .= " Setting to default milestone for this product, "; + $err .= "\"" . $product->default_milestone . "\".\n"; + } + push( @query, "target_milestone" ); + } + + # For priority, severity, opsys and platform we check that the one being + # imported is valid. If it is not we use the defaults set in the parameters. + if (defined( $bug_fields{'bug_severity'} ) + && check_field('bug_severity', scalar $bug_fields{'bug_severity'}, + \@::legal_severity, ERR_LEVEL) ) + { + push( @values, $bug_fields{'bug_severity'} ); + } + else { + push( @values, Param('defaultseverity') ); + $err .= "Unknown severity "; + $err .= ( defined $bug_fields{'bug_severity'} ) + ? $bug_fields{'bug_severity'} + : "unknown"; + $err .= ". Setting to default severity \""; + $err .= Param('defaultseverity') . "\".\n"; + } + push( @query, "bug_severity" ); + + if (defined( $bug_fields{'priority'} ) + && check_field('priority', scalar $bug_fields{'priority'}, + \@::legal_priority, ERR_LEVEL ) ) + { + push( @values, $bug_fields{'priority'} ); + } + else { + push( @values, Param('defaultpriority') ); + $err .= "Unknown priority "; + $err .= ( defined $bug_fields{'priority'} ) + ? $bug_fields{'priority'} + : "unknown"; + $err .= ". Setting to default priority \""; + $err .= Param('defaultpriority') . "\".\n"; + } + push( @query, "priority" ); + + if (defined( $bug_fields{'rep_platform'} ) + && check_field('rep_platform', scalar $bug_fields{'rep_platform'}, + \@::legal_platform, ERR_LEVEL ) ) + { + push( @values, $bug_fields{'rep_platform'} ); + } + else { + push( @values, Param('defaultplatform') ); + $err .= "Unknown platform "; + $err .= ( defined $bug_fields{'rep_platform'} ) + ? $bug_fields{'rep_platform'} + : "unknown"; + $err .=". Setting to default platform \""; + $err .= Param('defaultplatform') . "\".\n"; + } + push( @query, "rep_platform" ); + + if (defined( $bug_fields{'op_sys'} ) + && check_field('op_sys', scalar $bug_fields{'op_sys'}, + \@::legal_opsys, ERR_LEVEL ) ) + { + push( @values, $bug_fields{'op_sys'} ); + } + else { + push( @values, Param('defaultopsys') ); + $err .= "Unknown operating system "; + $err .= ( defined $bug_fields{'op_sys'} ) + ? $bug_fields{'op_sys'} + : "unknown"; + $err .= ". Setting to default OS \"" . Param('defaultopsys') . "\".\n"; + } + push( @query, "op_sys" ); + + # Process time fields + if ( Param("timetrackinggroup") ) { + my $date = format_time( $bug_fields{'deadline'}, "%Y-%m-%d" ) + || undef; + push( @values, $date ); + push( @query, "deadline" ); + eval { + Bugzilla::Bug::ValidateTime($bug_fields{'estimated_time'}, "e"); + }; + if (!$@){ + push( @values, $bug_fields{'estimated_time'} ); + push( @query, "estimated_time" ); + } + eval { + Bugzilla::Bug::ValidateTime($bug_fields{'remaining_time'}, "r"); + }; + if (!$@){ + push( @values, $bug_fields{'remaining_time'} ); + push( @query, "remaining_time" ); + } + eval { + Bugzilla::Bug::ValidateTime($bug_fields{'actual_time'}, "a"); + }; + if ($@){ + $bug_fields{'actual_time'} = 0.0; + $err .= "Invalid Actual Time. Setting to 0.0\n"; + } + } + + # Reporter Assignee QA Contact + my $exporterid = $exporter->id; + my $reporterid = login_to_id( $bug_fields{'reporter'} ) + if $bug_fields{'reporter'}; + push( @query, "reporter" ); + if ( ( $bug_fields{'reporter'} ) && ($reporterid) ) { + push( @values, $reporterid ); + } + else { + push( @values, $exporterid ); + $err .= "The original reporter of this bug does not have\n"; + $err .= " an account here. Reassigning to the person who moved\n"; + $err .= " it here: $exporter_login.\n"; + if ( $bug_fields{'reporter'} ) { + $err .= " Previous reporter was $bug_fields{'reporter'}.\n"; + } + else { + $err .= " Previous reporter is unknown.\n"; + } + } + + my $changed_owner = 0; + my $owner; + push( @query, "assigned_to" ); + if ( ( $bug_fields{'assigned_to'} ) + && ( $owner = login_to_id( $bug_fields{'assigned_to'} )) ) { + push( @values, $owner ); + } + else { + push( @values, $component->default_assignee->id ); + $changed_owner = 1; + $err .= "The original assignee of this bug does not have\n"; + $err .= " an account here. Reassigning to the default assignee\n"; + $err .= " for the component, ". $component->default_assignee->login .".\n"; + if ( $bug_fields{'assigned_to'} ) { + $err .= " Previous assignee was $bug_fields{'assigned_to'}.\n"; + } + else { + $err .= " Previous assignee is unknown.\n"; + } + } + + if ( Param("useqacontact") ) { + my $qa_contact; + push( @query, "qa_contact" ); + if ( ( defined $bug_fields{'qa_contact'}) + && ( $qa_contact = login_to_id( $bug_fields{'qa_contact'} ) ) ) { + push( @values, $qa_contact ); + } + else { + push( @values, $component->default_qa_contact->id || undef ); + if ($component->default_qa_contact->id){ + $err .= "Setting qa contact to the default for this product.\n"; + $err .= " This bug either had no qa contact or an invalid one.\n"; + } + } + } + + # Status & Resolution + my $has_res = defined($bug_fields{'resolution'}); + my $has_status = defined($bug_fields{'bug_status'}); + my $valid_res = check_field('resolution', + scalar $bug_fields{'resolution'}, + \@::legal_resolution, ERR_LEVEL ); + my $valid_status = check_field('bug_status', + scalar $bug_fields{'bug_status'}, + \@::legal_bug_status, ERR_LEVEL ); + my $is_open = IsOpenedState($bug_fields{'bug_status'}); + my $status = $bug_fields{'bug_status'} || undef; + my $resolution = $bug_fields{'resolution'} || undef; + + # Check everconfirmed + my $everconfirmed; + if ($product->votes_to_confirm) { + $everconfirmed = $bug_fields{'everconfirmed'} || 0; + } + else { + $everconfirmed = 1; + } + push (@query, "everconfirmed"); + push (@values, $everconfirmed); + + # Sanity check will complain about having bugs marked duplicate but no + # entry in the dup table. Since we can't tell the bug ID of bugs + # that might not yet be in the database we have no way of populating + # this table. Change the resolution instead. + if ( $valid_res && ( $bug_fields{'resolution'} eq "DUPLICATE" ) ) { + $resolution = "MOVED"; + $err .= "This bug was marked DUPLICATE in the database "; + $err .= "it was moved from.\n Changing resolution to \"MOVED\"\n"; + } + + if($has_status){ + if($valid_status){ + if($is_open){ + if($has_res){ + $err .= "Resolution set on an open status.\n"; + $err .= " Dropping resolution $resolution\n"; + $resolution = undef; + } + if($changed_owner){ + if($everconfirmed){ + $status = "NEW"; + } + else{ + $status = "UNCONFIRMED"; + } + if ($status ne $bug_fields{'bug_status'}){ + $err .= "Bug reassigned, setting status to \"$status\".\n"; + $err .= " Previous status was \""; + $err .= $bug_fields{'bug_status'} . "\".\n"; + } + } + if($everconfirmed){ + if($status eq "UNCONFIRMED"){ + $err .= "Bug Status was UNCONFIRMED but everconfirmed was true\n"; + $err .= " Setting status to NEW\n"; + $err .= "Resetting votes to 0\n" if ( $bug_fields{'votes'} ); + $status = "NEW"; + } + } + else{ # $everconfirmed is false + if($status ne "UNCONFIRMED"){ + $err .= "Bug Status was $status but everconfirmed was false\n"; + $err .= " Setting status to UNCONFIRMED\n"; + $status = "UNCONFIRMED"; + } + } + } + else{ # $is_open is false + if(!$has_res){ + $err .= "Missing Resolution. Setting status to "; + if($everconfirmed){ + $status = "NEW"; + $err .= "NEW\n"; + } + else{ + $status = "UNCONFIRMED"; + $err .= "UNCONFIRMED\n"; + } + } + if(!$valid_res){ + $err .= "Unknown resolution \"$resolution\".\n"; + $err .= " Setting resolution to MOVED\n"; + $resolution = "MOVED"; + } + } + } + else{ # $valid_status is false + if($everconfirmed){ + $status = "NEW"; + } + else{ + $status = "UNCONFIRMED"; + } + $err .= "Bug has invalid status, setting status to \"$status\".\n"; + $err .= " Previous status was \""; + $err .= $bug_fields{'bug_status'} . "\".\n"; + $resolution = undef; + } + + } + else{ #has_status is false + if($everconfirmed){ + $status = "NEW"; + } + else{ + $status = "UNCONFIRMED"; + } + $err .= "Bug has no status, setting status to \"$status\".\n"; + $err .= " Previous status was unknown\n"; + $resolution = undef; + } + + if (defined $resolution){ + push( @query, "resolution" ); + push( @values, $resolution ); + } + + # Bug status + push( @query, "bug_status" ); + push( @values, $status ); + + + # For the sake of sanitycheck.cgi we do this. + # Update lastdiffed if you do not want to have mail sent + unless ($mail) { + push @query, "lastdiffed"; + push @values, $timestamp; + } + + # INSERT the bug + my $query = "INSERT INTO bugs (" . join( ", ", @query ) . ") VALUES ("; + $query .= '?,' foreach (@values); + chop($query); # Remove the last comma. + $query .= ")"; + + $dbh->do( $query, undef, @values ); + my $id = $dbh->bz_last_key( 'bugs', 'bug_id' ); + + # We are almost certain to get some uninitialized warnings + # Since this is just for debugging the query, let's shut them up + eval { + no warnings 'uninitialized'; + Debug( + "Bug Query: INSERT INTO bugs (\n" + . join( ",\n", @query ) + . "\n) VALUES (\n" + . join( ",\n", @values ), + DEBUG_LEVEL + ); + }; + + # Handle CC's + if ( defined $bug_fields{'cc'} ) { + my %ccseen; + my $sth_cc = $dbh->prepare("INSERT INTO cc (bug_id, who) VALUES (?,?)"); + foreach my $person ( split( /[\s,]+/, $bug_fields{'cc'} ) ) { + next unless $person; + my $uid; + if ($uid = login_to_id($person)) { + if ( !$ccseen{$uid} ) { + $sth_cc->execute( $id, $uid ); + $ccseen{$uid} = 1; + } + } + else { + $err .= "CC member $person does not have an account here\n"; + } + } + } + + # Handle keywords + if ( defined( $bug_fields{'keywords'} ) ) { + my %keywordseen; + my $key_sth = $dbh->prepare( + "INSERT INTO keywords + (bug_id, keywordid) VALUES (?,?)" + ); + foreach my $keyword ( split( /[\s,]+/, $bug_fields{'keywords'} )) { + next unless $keyword; + my $i = GetKeywordIdFromName($keyword); + if ( !$i ) { + $err .= "Skipping unknown keyword: $keyword.\n"; + next; + } + if ( !$keywordseen{$i} ) { + $key_sth->execute( $id, $i ); + $keywordseen{$i} = 1; + } + } + my ($keywordarray) = $dbh->selectcol_arrayref( + "SELECT d.name FROM keyworddefs d + INNER JOIN keywords k + ON d.id = k.keywordid + WHERE k.bug_id = ? + ORDER BY d.name", undef, $id); + my $keywordstring = join( ", ", @{$keywordarray} ); + $dbh->do( "UPDATE bugs SET keywords = ? WHERE bug_id = ?", + undef, $keywordstring, $id ) + } + + # Parse bug flags + foreach my $bflag ( $bug->children('flag')) { + next unless ( defined($bflag) ); + $err .= flag_handler( + $bflag->{'att'}->{'name'}, $bflag->{'att'}->{'status'}, + $bflag->{'att'}->{'setter'}, $bflag->{'att'}->{'requestee'}, + $exporterid, $id, + $comp_id, $prod_id, + undef + ); + } + + # Insert Attachments for the bug + foreach my $att (@attachments) { + if ($att eq "err"){ + $err .= "No attachment ID specified, dropping attachment\n"; + next; + } + if (!$exporter->in_group(Param('insidergroup')) && $att->{'isprivate'}){ + $err .= "Exporter not in insidergroup and attachment marked private.\n"; + $err .= " Marking attachment public\n"; + $att->{'isprivate'} = 0; + } + $dbh->do("INSERT INTO attachments + (bug_id, creation_ts, filename, description, mimetype, + ispatch, isprivate, isobsolete, submitter_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + undef, $id, $att->{'date'}, $att->{'filename'}, + $att->{'desc'}, $att->{'ctype'}, $att->{'ispatch'}, + $att->{'isprivate'}, $att->{'isobsolete'}, $exporterid); + my $att_id = $dbh->bz_last_key( 'attachments', 'attach_id' ); + my $att_data = $att->{'data'}; + my $sth = $dbh->prepare("INSERT INTO attach_data (id, thedata) + VALUES ($att_id, ?)" ); + trick_taint($att_data); + $sth->bind_param( 1, $att_data, $dbh->BLOB_TYPE ); + $sth->execute(); + $comments .= "Imported an attachment (id=$att_id)\n"; + + # Process attachment flags + foreach my $aflag (@{ $att->{'flags'} }) { + next unless defined($aflag) ; + $err .= flag_handler( + $aflag->{'name'}, $aflag->{'status'}, + $aflag->{'setter'}, $aflag->{'requestee'}, + $exporterid, $id, + $comp_id, $prod_id, + $att_id + ); + } + } + + # Clear the attachments array for the next bug + @attachments = (); + + # Insert longdesc and append any errors + my $worktime = $bug_fields{'actual_time'} || 0.0; + $worktime = 0.0 if (!$exporter->in_group(Param('timetrackinggroup'))); + $long_description .= "\n" . $comments; + if ($err) { + $long_description .= "\n$err\n"; + } + trick_taint($long_description); + $dbh->do("INSERT INTO longdescs + (bug_id, who, bug_when, work_time, isprivate, thetext) + VALUES (?,?,?,?,?,?)", undef, + $id, $exporterid, $timestamp, $worktime, $private, $long_description + ); + + # Add this bug to each group of which its product is a member. + my $sth_group = $dbh->prepare("INSERT INTO bug_group_map (bug_id, group_id) + VALUES (?, ?)"); + foreach my $group_id ( keys %{ $product->group_controls } ) { + if ($product->group_controls->{$group_id}->{'membercontrol'} != CONTROLMAPNA + && $product->group_controls->{$group_id}->{'othercontrol'} != CONTROLMAPNA){ + $sth_group->execute( $id, $group_id ); + } + } + + $log .= "Bug ${urlbase}show_bug.cgi?id=$bug_fields{'bug_id'} "; + $log .= "imported as bug $id.\n"; + $log .= Param("urlbase") . "show_bug.cgi?id=$id\n\n"; + if ($err) { + $log .= "The following problems were encountered while creating bug $id.\n"; + $log .= $err; + $log .= "You may have to set certain fields in the new bug by hand.\n\n"; + } + Debug( $log, OK_LEVEL ); + Bugzilla::BugMail::Send( $id, { 'changer' => $exporter_login } ) if ($mail); + + # done with the xml data. Lets clear it from memory + $twig->purge; + } -my $subject = "$bugqty bug(s) successfully moved from $urlbase to " - . Param("urlbase") ; -my @to = ($exporter); -MailMessage ($subject, $log, @to); +Debug( "Reading xml", DEBUG_LEVEL ); + +# Read STDIN in slurp mode. VERY dangerous, but we live on the wild side ;-) +local ($/); +$xml = <>; + +# If the email was encoded (BugMail::MessageToMTA() does it when using UTF-8), +# we have to decode it first, else the XML parsing will fail. +my $parser = MIME::Parser->new; +$parser->output_to_core(1); +$parser->tmp_to_core(1); +my $entity = $parser->parse_data($xml); +my $bodyhandle = $entity->bodyhandle; +$xml = $bodyhandle->as_string; + +# remove everything in file before xml header (i.e. remove the mail header) +$xml =~ s/^.+(<\?xml version.+)$/$1/s; + +Debug( "Parsing tree", DEBUG_LEVEL ); +my $twig = XML::Twig->new( + twig_handlers => { + bug => \&process_bug, + attachment => \&process_attachment + }, + start_tag_handlers => { bugzilla => \&init } +); +$twig->parse($xml); +my $root = $twig->root; +my $maintainer = $root->{'att'}->{'maintainer'}; +my $exporter = $root->{'att'}->{'exporter'}; +my $urlbase = $root->{'att'}->{'urlbase'}; +$log .= "Imported $bugtotal bug(s) from $urlbase,\n sent by $exporter.\n\n"; +my $subject = "$bugtotal Bug(s) successfully moved from $urlbase to " + . Param("urlbase"); +my @to = ($exporter, $maintainer); +MailMessage( $subject, $log, @to ); + +__END__ + +=head1 NAME + +importxml - Import bugzilla bug data from xml. + +=head1 SYNOPSIS + + importxml.pl [options] [file ...] + + Options: + -? --help brief help message + -v --verbose print error and debug information. + Mulltiple -v increases verbosity + -m --sendmail send mail to recipients with log of bugs imported + +=head1 OPTIONS + +=over 8 + +=item B<-?> + + Print a brief help message and exits. + +=item B<-v> + + Print error and debug information. Mulltiple -v increases verbosity + +=item B<-m> + + Send mail to exporter with a log of bugs imported and any errors. + +=back + +=head1 DESCRIPTION + + This script is used to import bugs from another installation of bugzilla. + It can be used in two ways. + First using the move function of bugzilla + on another system will send mail to an alias provided by + the administrator of the target installation (you). Set up an alias + similar to the one given below so this mail will be automatically + run by this script and imported into your database. Run 'newaliases' + after adding this alias to your aliases file. Make sure your sendmail + installation is configured to allow mail aliases to execute code. + + bugzilla-import: "|/usr/bin/perl /opt/bugzilla/importxml.pl --mail" + + Second it can be run from the command line with any xml file from + STDIN that conforms to the bugzilla DTD. In this case you can pass + an argument to set whether you want to send the + mail that will be sent to the exporter and maintainer normally. + + importxml.pl [options] bugsfile.xml + +=cut + diff --git a/t/Support/Files.pm b/t/Support/Files.pm index d52380310..4f1b619b4 100644 --- a/t/Support/Files.pm +++ b/t/Support/Files.pm @@ -30,7 +30,7 @@ use File::Find; # @additional_files = (); %exclude_deps = ( - 'XML::Parser' => ['importxml.pl'], + 'XML::Twig' => ['importxml.pl'], 'Net::LDAP' => ['Bugzilla/Auth/Verify/LDAP.pm'], ); |