]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 285614: Rewrite importxml.pl to remove XML::Parser magic numbers - Patch by Greg...
authorlpsolit%gmail.com <>
Wed, 11 Jan 2006 07:53:07 +0000 (07:53 +0000)
committerlpsolit%gmail.com <>
Wed, 11 Jan 2006 07:53:07 +0000 (07:53 +0000)
checksetup.pl
importxml.pl
t/Support/Files.pm

index 56db99b467ea308f79fa94ffae61cec73a6e9f98..847ada5bbf81ab110cc3c17914c3ebb50c63b3dd 100755 (executable)
@@ -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",
index 9c56b3d1a3856f45b1e5bc7d02c87b976a9d61fb..e3a1169216fb1c8aeb50eb237fab1a22fd43f2b1 100755 (executable)
@@ -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
 # 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/&amp;/&/g;
-    $_[0] =~ s/&lt;/</g;
-    $_[0] =~ s/&gt;/>/g;
-    $_[0] =~ s/&apos;/'/g;  # ' # Darned emacs colors
-    $_[0] =~ s/&quot;/"/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
+
index d52380310e6b1b82c85055134c70d682b02f2811..4f1b619b4f5f6992c6357a35e19a32d0e4d89431 100644 (file)
@@ -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'],
 );