2010-08-30 Alejandro G. Castro <alex@igalia.com>
[WebKit-https.git] / BugsSite / importxml.pl
1 #!/usr/bin/env perl -wT
2 # -*- Mode: perl; indent-tabs-mode: nil -*-
3 #
4 # The contents of this file are subject to the Mozilla Public
5 # License Version 1.1 (the "License"); you may not use this file
6 # except in compliance with the License. You may obtain a copy of
7 # the License at http://www.mozilla.org/MPL/
8 #
9 # Software distributed under the License is distributed on an "AS
10 # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
11 # implied. See the License for the specific language governing
12 # rights and limitations under the License.
13 #
14 # The Original Code is the Bugzilla Bug Tracking System.
15 #
16 # The Initial Developer of the Original Code is Netscape Communications
17 # Corporation. Portions created by Netscape are
18 # Copyright (C) 1998 Netscape Communications Corporation. All
19 # Rights Reserved.
20 #
21 # Contributor(s): Dawn Endico <endico@mozilla.org>
22 #                 Gregary Hendricks <ghendricks@novell.com>
23 #                 Vance Baarda <vrb@novell.com>
24 #                 Guzman Braso <gbn@hqso.net>
25 #                 Erik Purins <epurins@day1studios.com>
26 #                 Frédéric Buclin <LpSolit@gmail.com>
27
28 # This script reads in xml bug data from standard input and inserts
29 # a new bug into bugzilla. Everything before the beginning <?xml line
30 # is removed so you can pipe in email messages.
31
32 use strict;
33
34 #####################################################################
35 #
36 # This script is used to import bugs from another installation of bugzilla.
37 # It can be used in two ways.
38 # First using the move function of bugzilla
39 # on another system will send mail to an alias provided by
40 # the administrator of the target installation (you). Set up an alias
41 # similar to the one given below so this mail will be automatically
42 # run by this script and imported into your database.  Run 'newaliases'
43 # after adding this alias to your aliases file. Make sure your sendmail
44 # installation is configured to allow mail aliases to execute code.
45 #
46 # bugzilla-import: "|/usr/bin/perl /opt/bugzilla/importxml.pl"
47 #
48 # Second it can be run from the command line with any xml file from
49 # STDIN that conforms to the bugzilla DTD. In this case you can pass
50 # an argument to set whether you want to send the
51 # mail that will be sent to the exporter and maintainer normally.
52 #
53 # importxml.pl bugsfile.xml
54 #
55 #####################################################################
56
57 use File::Basename qw(dirname);
58 # MTAs may call this script from any directory, but it should always
59 # run from this one so that it can find its modules.
60 BEGIN {
61     require File::Basename;
62     my $dir = $0; $dir =~ /(.*)/; $dir = $1; # trick taint
63     chdir(File::Basename::dirname($dir));
64 }
65
66 use lib qw(. lib);
67 # Data dumber is used for debugging, I got tired of copying it back in 
68 # and then removing it. 
69 #use Data::Dumper;
70
71
72 use Bugzilla;
73 use Bugzilla::Bug;
74 use Bugzilla::Product;
75 use Bugzilla::Version;
76 use Bugzilla::Component;
77 use Bugzilla::Milestone;
78 use Bugzilla::FlagType;
79 use Bugzilla::BugMail;
80 use Bugzilla::Mailer;
81 use Bugzilla::User;
82 use Bugzilla::Util;
83 use Bugzilla::Constants;
84 use Bugzilla::Keyword;
85 use Bugzilla::Field;
86 use Bugzilla::Status;
87
88 use MIME::Base64;
89 use MIME::Parser;
90 use Date::Format;
91 use Getopt::Long;
92 use Pod::Usage;
93 use XML::Twig;
94
95 # We want to capture errors and handle them here rather than have the Template
96 # code barf all over the place.
97 Bugzilla->usage_mode(Bugzilla::Constants::USAGE_MODE_CMDLINE);
98
99 my $debug = 0;
100 my $mail  = '';
101 my $attach_path = '';
102 my $help  = 0;
103
104 my $result = GetOptions(
105     "verbose|debug+" => \$debug,
106     "mail|sendmail!" => \$mail,
107     "attach_path=s"  => \$attach_path,
108     "help|?"         => \$help
109 );
110
111 pod2usage(0) if $help;
112
113 use constant OK_LEVEL    => 3;
114 use constant DEBUG_LEVEL => 2;
115 use constant ERR_LEVEL   => 1;
116
117 our @logs;
118 our @attachments;
119 our $bugtotal;
120 my $xml;
121 my $dbh = Bugzilla->dbh;
122 my $params = Bugzilla->params;
123 my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
124
125 ###############################################################################
126 # Helper sub routines                                                         #
127 ###############################################################################
128
129 sub MailMessage {
130     return unless ($mail);
131     my $subject    = shift;
132     my $message    = shift;
133     my @recipients = @_;
134     my $from   = $params->{"moved-from-address"};
135     $from =~ s/@/\@/g;
136
137     foreach my $to (@recipients){
138         my $header = "To: $to\n";
139         $header .= "From: Bugzilla <$from>\n";
140         $header .= "Subject: $subject\n\n";
141         my $sendmessage = $header . $message . "\n";
142         MessageToMTA($sendmessage);
143     }
144
145 }
146
147 sub Debug {
148     return unless ($debug);
149     my ( $message, $level ) = (@_);
150     print STDERR "OK: $message \n" if ( $level == OK_LEVEL );
151     print STDERR "ERR: $message \n" if ( $level == ERR_LEVEL );
152     print STDERR "$message\n"
153       if ( ( $debug == $level ) && ( $level == DEBUG_LEVEL ) );
154 }
155
156 sub Error {
157     my ( $reason, $errtype, $exporter ) = @_;
158     my $subject = "Bug import error: $reason";
159     my $message = "Cannot import these bugs because $reason ";
160     $message .= "\n\nPlease re-open the original bug.\n" if ($errtype);
161     $message .= "For more info, contact " . $params->{"maintainer"} . ".\n";
162     my @to = ( $params->{"maintainer"}, $exporter);
163     Debug( $message, ERR_LEVEL );
164     MailMessage( $subject, $message, @to );
165     exit;
166 }
167
168 # This subroutine handles flags for process_bug. It is generic in that
169 # it can handle both attachment flags and bug flags.
170 sub flag_handler {
171     my (
172         $name,            $status,      $setter_login,
173         $requestee_login, $exporterid,  $bugid,
174         $productid,       $componentid, $attachid
175       )
176       = @_;
177
178     my $type         = ($attachid) ? "attachment" : "bug";
179     my $err          = '';
180     my $setter       = new Bugzilla::User({ name => $setter_login });
181     my $requestee;
182     my $requestee_id;
183
184     unless ($setter) {
185         $err = "Invalid setter $setter_login on $type flag $name\n";
186         $err .= "   Dropping flag $name\n";
187         return $err;
188     }
189     if ( !$setter->can_see_bug($bugid) ) {
190         $err .= "Setter is not a member of bug group\n";
191         $err .= "   Dropping flag $name\n";
192         return $err;
193     }
194     my $setter_id = $setter->id;
195     if ( defined($requestee_login) ) {
196         $requestee = new Bugzilla::User({ name => $requestee_login });
197         if ( $requestee ) {
198             if ( !$requestee->can_see_bug($bugid) ) {
199                 $err .= "Requestee is not a member of bug group\n";
200                 $err .= "   Requesting from the wind\n";
201             }    
202             else{
203                 $requestee_id = $requestee->id;
204             }
205         }
206         else {
207             $err = "Invalid requestee $requestee_login on $type flag $name\n";
208             $err .= "   Requesting from the wind.\n";
209         }
210         
211     }
212     my $flag_types;
213
214     # If this is an attachment flag we need to do some dirty work to look
215     # up the flagtype ID
216     if ($attachid) {
217         $flag_types = Bugzilla::FlagType::match(
218             {
219                 'target_type'  => 'attachment',
220                 'product_id'   => $productid,
221                 'component_id' => $componentid
222             } );
223     }
224     else {
225         my $bug = new Bugzilla::Bug($bugid);
226         $flag_types = $bug->flag_types;
227     }
228     unless ($flag_types){
229         $err  = "No flag types defined for this bug\n";
230         $err .= "   Dropping flag $name\n";
231         return $err;
232     }
233
234     # We need to see if the imported flag is in the list of known flags
235     # It is possible for two flags on the same bug have the same name
236     # If this is the case, we will only match the first one.
237     my $ftype;
238     foreach my $f ( @{$flag_types} ) {
239         if ( $f->name eq $name) {
240             $ftype = $f;
241             last;
242         }
243     }
244
245     if ($ftype) {    # We found the flag in the list
246         my $grant_group = $ftype->grant_group;
247         if (( $status eq '+' || $status eq '-' ) 
248             && $grant_group && !$setter->in_group_id($grant_group->id)) {
249             $err = "Setter $setter_login on $type flag $name ";
250             $err .= "is not in the Grant Group\n";
251             $err .= "   Dropping flag $name\n";
252             return $err;
253         }
254         my $request_group = $ftype->request_group;
255         if ($request_group
256             && $status eq '?' && !$setter->in_group_id($request_group->id)) {
257             $err = "Setter $setter_login on $type flag $name ";
258             $err .= "is not in the Request Group\n";
259             $err .= "   Dropping flag $name\n";
260             return $err;
261         }
262
263         # Take the first flag_type that matches
264         unless ($ftype->is_active) {
265             $err = "Flag $name is not active in this database\n";
266             $err .= "   Dropping flag $name\n";
267             return $err;
268         }
269
270         $dbh->do("INSERT INTO flags 
271                  (type_id, status, bug_id, attach_id, creation_date, 
272                   setter_id, requestee_id)
273                   VALUES (?, ?, ?, ?, ?, ?, ?)", undef,
274             ($ftype->id, $status, $bugid, $attachid, $timestamp,
275             $setter_id, $requestee_id));
276     }
277     else {
278         $err = "Dropping unknown $type flag: $name\n";
279         return $err;
280     }
281     return $err;
282 }
283
284 # Converts and returns the input data as an array.
285 sub _to_array {
286     my $value = shift;
287
288     $value = [$value] if !ref($value);
289     return @$value;
290 }
291
292 ###############################################################################
293 # XML Handlers                                                                #
294 ###############################################################################
295
296 # This subroutine gets called only once - as soon as the <bugzilla> opening
297 # tag is parsed. It simply checks to see that the all important exporter
298 # maintainer and URL base are set.
299 #
300 #    exporter:   email address of the person moving the bugs
301 #    maintainer: the maintainer of the bugzilla installation
302 #                as set in the parameters file
303 #    urlbase:    The urlbase parameter of the installation
304 #                bugs are being moved from
305 #
306 sub init() {
307     my ( $twig, $bugzilla ) = @_;
308     my $root       = $twig->root;
309     my $maintainer = $root->{'att'}->{'maintainer'};
310     my $exporter   = $root->{'att'}->{'exporter'};
311     my $urlbase    = $root->{'att'}->{'urlbase'};
312     my $xmlversion = $root->{'att'}->{'version'};
313
314     if ($xmlversion ne BUGZILLA_VERSION) {
315             my $log = "Possible version conflict!\n";
316             $log .= "   XML was exported from Bugzilla version $xmlversion\n";
317             $log .= "   But this installation uses ";
318             $log .= BUGZILLA_VERSION . "\n";
319             Debug($log, OK_LEVEL);
320             push(@logs, $log);
321     }
322     Error( "no maintainer", "REOPEN", $exporter ) unless ($maintainer);
323     Error( "no exporter",   "REOPEN", $exporter ) unless ($exporter);
324     Error( "bug importing is disabled here", undef, $exporter ) unless ( $params->{"move-enabled"} );
325     Error( "invalid exporter: $exporter", "REOPEN", $exporter ) if ( !login_to_id($exporter) );
326     Error( "no urlbase set", "REOPEN", $exporter ) unless ($urlbase);
327     my $def_product =
328         new Bugzilla::Product( { name => $params->{"moved-default-product"} } )
329         || Error("an invalid default product was defined for the target DB. " .
330                   $params->{"maintainer"} . " needs to fix the definitions of " .
331                  "moved-default-product. \n", "REOPEN", $exporter);
332     my $def_component = new Bugzilla::Component(
333         {
334             product => $def_product,
335             name    => $params->{"moved-default-component"}
336         })
337     || Error("an invalid default component was defined for the target DB. " .
338              $params->{"maintainer"} . " needs to fix the definitions of " .
339              "moved-default-component.\n", "REOPEN", $exporter);
340 }
341     
342
343 # Parse attachments.
344 #
345 # This subroutine is called once for each attachment in the xml file.
346 # It is called as soon as the closing </attachment> tag is parsed.
347 # Since attachments have the potential to be very large, and
348 # since each attachment will be inside <bug>..</bug> tags we shove
349 # the attachment onto an array which will be processed by process_bug
350 # and then disposed of. The attachment array will then contain only
351 # one bugs' attachments at a time.
352 # The cycle will then repeat for the next <bug>
353 #
354 # The attach_id is ignored since mysql generates a new one for us.
355 # The submitter_id gets filled in with $exporterid.
356
357 sub process_attachment() {
358     my ( $twig, $attach ) = @_;
359     Debug( "Parsing attachments", DEBUG_LEVEL );
360     my %attachment;
361
362     $attachment{'date'} =
363         format_time( $attach->field('date'), "%Y-%m-%d %R" ) || $timestamp;
364     $attachment{'desc'}       = $attach->field('desc');
365     $attachment{'ctype'}      = $attach->field('type') || "unknown/unknown";
366     $attachment{'attachid'}   = $attach->field('attachid');
367     $attachment{'ispatch'}    = $attach->{'att'}->{'ispatch'} || 0;
368     $attachment{'isobsolete'} = $attach->{'att'}->{'isobsolete'} || 0;
369     $attachment{'isprivate'}  = $attach->{'att'}->{'isprivate'} || 0;
370     $attachment{'filename'}   = $attach->field('filename') || "file";
371     $attachment{'attacher'}   = $attach->field('attacher');
372     # Attachment data is not exported in versions 2.20 and older.
373     if (defined $attach->first_child('data') &&
374             defined $attach->first_child('data')->{'att'}->{'encoding'}) {
375         my $encoding = $attach->first_child('data')->{'att'}->{'encoding'};
376         if ($encoding =~ /base64/) {
377             # decode the base64
378             my $data   = $attach->field('data');
379             my $output = decode_base64($data);
380             $attachment{'data'} = $output;
381         }
382         elsif ($encoding =~ /filename/) {
383             # read the attachment file
384             Error("attach_path is required", undef) unless ($attach_path);
385             
386             my $filename = $attach->field('data');
387             # Remove any leading path data from the filename
388             $filename =~ s/(.*\/|.*\\)//gs;
389             
390             my $attach_filename = $attach_path . "/" . $filename;
391             open(ATTACH_FH, "<", $attach_filename) or
392                 Error("cannot open $attach_filename", undef);
393             $attachment{'data'} = do { local $/; <ATTACH_FH> };
394             close ATTACH_FH;
395         }
396     }
397     else {
398         $attachment{'data'} = $attach->field('data');
399     }
400
401     # attachment flags
402     my @aflags;
403     foreach my $aflag ( $attach->children('flag') ) {
404         my %aflag;
405         $aflag{'name'}      = $aflag->{'att'}->{'name'};
406         $aflag{'status'}    = $aflag->{'att'}->{'status'};
407         $aflag{'setter'}    = $aflag->{'att'}->{'setter'};
408         $aflag{'requestee'} = $aflag->{'att'}->{'requestee'};
409         push @aflags, \%aflag;
410     }
411     $attachment{'flags'} = \@aflags if (@aflags);
412
413     # free up the memory for use by the rest of the script
414     $attach->delete;
415     if ($attachment{'attachid'}) {
416         push @attachments, \%attachment;
417     }
418     else {
419         push @attachments, "err";
420     }
421 }
422
423 # This subroutine will be called once for each <bug> in the xml file.
424 # It is called as soon as the closing </bug> tag is parsed.
425 # If this bug had any <attachment> tags, they will have been processed
426 # before we get to this point and their data will be in the @attachments
427 # array.
428 # As each bug is processed, it is inserted into the database and then
429 # purged from memory to free it up for later bugs.
430
431 sub process_bug {
432     my ( $twig, $bug ) = @_;
433     my $root             = $twig->root;
434     my $maintainer       = $root->{'att'}->{'maintainer'};
435     my $exporter_login   = $root->{'att'}->{'exporter'};
436     my $exporter         = new Bugzilla::User({ name => $exporter_login });
437     my $urlbase          = $root->{'att'}->{'urlbase'};
438
439     # We will store output information in this variable.
440     my $log = "";
441     if ( defined $bug->{'att'}->{'error'} ) {
442         $log .= "\nError in bug " . $bug->field('bug_id') . "\@$urlbase: ";
443         $log .= $bug->{'att'}->{'error'} . "\n";
444         if ( $bug->{'att'}->{'error'} =~ /NotFound/ ) {
445             $log .= "$exporter_login tried to move bug " . $bug->field('bug_id');
446             $log .= " here, but $urlbase reports that this bug";
447             $log .= " does not exist.\n";
448         }
449         elsif ( $bug->{'att'}->{'error'} =~ /NotPermitted/ ) {
450             $log .= "$exporter_login tried to move bug " . $bug->field('bug_id');
451             $log .= " here, but $urlbase reports that $exporter_login does ";
452             $log .= " not have access to that bug.\n";
453         }
454         return;
455     }
456     $bugtotal++;
457
458     # This list contains all other bug fields that we want to process.
459     # If it is not in this list it will not be included.
460     my %all_fields;
461     foreach my $field ( 
462         qw(long_desc attachment flag group), Bugzilla::Bug::fields() )
463     {
464         $all_fields{$field} = 1;
465     }
466     
467     my %bug_fields;
468     my $err = "";
469
470    # Loop through all the xml tags inside a <bug> and compare them to the
471    # lists of fields. If they match throw them into the hash. Otherwise
472    # append it to the log, which will go into the comments when we are done.
473     foreach my $bugchild ( $bug->children() ) {
474         Debug( "Parsing field: " . $bugchild->name, DEBUG_LEVEL );
475
476         # Skip the token if one is included. We don't want it included in
477         # the comments, and it is not used by the importer.
478         next if $bugchild->name eq 'token';
479
480         if ( defined $all_fields{ $bugchild->name } ) {
481             my @values = $bug->children_text($bugchild->name);
482             if (scalar @values > 1) {
483                 $bug_fields{$bugchild->name} = \@values;
484             }
485             else {
486                 $bug_fields{$bugchild->name} = $values[0];
487             }
488         }
489         else {
490             $err .= "Unknown bug field \"" . $bugchild->name . "\"";
491             $err .= " encountered while moving bug\n";
492             $err .= "   <" . $bugchild->name . ">";
493             if ( $bugchild->children_count > 1 ) {
494                 $err .= "\n";
495                 foreach my $subchild ( $bugchild->children() ) {
496                     $err .= "     <" . $subchild->name . ">";
497                     $err .= $subchild->field;
498                     $err .= "</" . $subchild->name . ">\n";
499                 }
500             }
501             else {
502                 $err .= $bugchild->field;
503             }
504             $err .= "</" . $bugchild->name . ">\n";
505         }
506     }
507
508     my @long_descs;
509     my $private = 0;
510
511     # Parse long descriptions
512     foreach my $comment ( $bug->children('long_desc') ) {
513         Debug( "Parsing Long Description", DEBUG_LEVEL );
514         my %long_desc;
515         $long_desc{'who'}       = $comment->field('who');
516         $long_desc{'bug_when'}  = $comment->field('bug_when');
517         $long_desc{'isprivate'} = $comment->{'att'}->{'isprivate'} || 0;
518
519         # if one of the comments is private we need to set this flag
520         if ( $long_desc{'isprivate'} && $exporter->in_group($params->{'insidergroup'})) {
521             $private = 1;
522         }
523         my $data = $comment->field('thetext');
524         if ( defined $comment->first_child('thetext')->{'att'}->{'encoding'}
525             && $comment->first_child('thetext')->{'att'}->{'encoding'} =~
526             /base64/ )
527         {
528             $data = decode_base64($data);
529         }
530
531         # If we leave the attachment ID in the comment it will be made a link
532         # to the wrong attachment. Since the new attachment ID is unknown yet
533         # let's strip it out for now. We will make a comment with the right ID
534         # later
535         $data =~ s/Created an attachment \(id=\d+\)/Created an attachment/g;
536
537         # Same goes for bug #'s Since we don't know if the referenced bug
538         # is also being moved, lets make sure they know it means a different
539         # bugzilla.
540         my $url = $urlbase . "show_bug.cgi?id=";
541         $data =~ s/([Bb]ugs?\s*\#?\s*(\d+))/$url$2/g;
542
543         $long_desc{'thetext'} = $data;
544         push @long_descs, \%long_desc;
545     }
546
547     # instead of giving each comment its own item in the longdescs
548     # table like it should have, lets cat them all into one big
549     # comment otherwise we would have to lie often about who
550     # authored the comment since commenters in one bugzilla probably
551     # don't have accounts in the other one.
552     # If one of the comments is private the whole comment will be
553     # private since we don't want to expose these unnecessarily
554     sub by_date { my @a; my @b; $a->{'bug_when'} cmp $b->{'bug_when'}; }
555     my @sorted_descs     = sort by_date @long_descs;
556     my $long_description = "";
557     for ( my $z = 0 ; $z <= $#sorted_descs ; $z++ ) {
558         if ( $z == 0 ) {
559             $long_description .= "\n\n\n---- Reported by ";
560         }
561         else {
562             $long_description .= "\n\n\n---- Additional Comments From ";
563         }
564         $long_description .= "$sorted_descs[$z]->{'who'} ";
565         $long_description .= "$sorted_descs[$z]->{'bug_when'}";
566         $long_description .= " ----";
567         $long_description .= "\n\n";
568         $long_description .= "THIS COMMENT IS PRIVATE \n"
569           if ( $sorted_descs[$z]->{'isprivate'} );
570         $long_description .= $sorted_descs[$z]->{'thetext'};
571         $long_description .= "\n";
572     }
573
574     my $comments;
575
576     $comments .= "\n\n--- Bug imported by $exporter_login ";
577     $comments .= time2str( "%Y-%m-%d %H:%M", time ) . " ";
578     $comments .= $params->{'timezone'};
579     $comments .= " ---\n\n";
580     $comments .= "This bug was previously known as _bug_ $bug_fields{'bug_id'} at ";
581     $comments .= $urlbase . "show_bug.cgi?id=" . $bug_fields{'bug_id'} . "\n";
582     if ( defined $bug_fields{'dependson'} ) {
583         $comments .= "This bug depended on bug(s) " .
584                      join(' ', _to_array($bug_fields{'dependson'})) . ".\n";
585     }
586     if ( defined $bug_fields{'blocked'} ) {
587         $comments .= "This bug blocked bug(s) " .
588                      join(' ', _to_array($bug_fields{'blocked'})) . ".\n";
589     }
590
591     # Now we process each of the fields in turn and make sure they contain
592     # valid data. We will create two parallel arrays, one for the query
593     # and one for the values. For every field we need to push an entry onto
594     # each array.
595     my @query  = ();
596     my @values = ();
597
598     # Each of these fields we will check for newlines and shove onto the array
599     foreach my $field (qw(status_whiteboard bug_file_loc short_desc)) {
600         if ($bug_fields{$field}) {
601             $bug_fields{$field} = clean_text( $bug_fields{$field} );
602             push( @query,  $field );
603             push( @values, $bug_fields{$field} );
604         }
605     }
606
607     # Alias
608     if ( $bug_fields{'alias'} ) {
609         my ($alias) = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs 
610                                                 WHERE alias = ?", undef,
611                                                 $bug_fields{'alias'} );
612         if ($alias) {
613             $err .= "Dropping conflicting bug alias ";
614             $err .= $bug_fields{'alias'} . "\n";
615         }
616         else {
617             $alias = $bug_fields{'alias'};
618             push @query,  'alias';
619             push @values, $alias;
620         }
621     }
622
623     # Timestamps
624     push( @query, "creation_ts" );
625     push( @values,
626         format_time( $bug_fields{'creation_ts'}, "%Y-%m-%d %X" )
627           || $timestamp );
628
629     push( @query, "delta_ts" );
630     push( @values,
631         format_time( $bug_fields{'delta_ts'}, "%Y-%m-%d %X" )
632           || $timestamp );
633
634     # Bug Access
635     push( @query,  "cclist_accessible" );
636     push( @values, $bug_fields{'cclist_accessible'} ? 1 : 0 );
637
638     push( @query,  "reporter_accessible" );
639     push( @values, $bug_fields{'reporter_accessible'} ? 1 : 0 );
640
641     # Product and Component if there is no valid default product and
642     # component defined in the parameters, we wouldn't be here
643     my $def_product =
644       new Bugzilla::Product( { name => $params->{"moved-default-product"} } );
645     my $def_component = new Bugzilla::Component(
646         {
647             product => $def_product,
648             name    => $params->{"moved-default-component"}
649         }
650     );
651     my $product;
652     my $component;
653
654     if ( defined $bug_fields{'product'} ) {
655         $product = new Bugzilla::Product( { name => $bug_fields{'product'} } );
656         unless ($product) {
657             $product = $def_product;
658             $err .= "Unknown Product " . $bug_fields{'product'} . "\n";
659             $err .= "   Using default product set in Parameters \n";
660         }
661     }
662     else {
663         $product = $def_product;
664     }
665     if ( defined $bug_fields{'component'} ) {
666         $component = new Bugzilla::Component(
667             {
668                 product => $product,
669                 name    => $bug_fields{'component'}
670             }
671         );
672         unless ($component) {
673             $component = $def_component;
674             $product   = $def_product;
675             $err .= "Unknown Component " . $bug_fields{'component'} . "\n";
676             $err .= "   Using default product and component set ";
677             $err .= "in Parameters \n";
678         }
679     }
680     else {
681         $component = $def_component;
682         $product   = $def_product;
683     }
684
685     my $prod_id = $product->id;
686     my $comp_id = $component->id;
687
688     push( @query,  "product_id" );
689     push( @values, $prod_id );
690     push( @query,  "component_id" );
691     push( @values, $comp_id );
692
693     # Since there is no default version for a product, we check that the one
694     # coming over is valid. If not we will use the first one in @versions
695     # and warn them.
696     my $version = new Bugzilla::Version(
697           { product => $product, name => $bug_fields{'version'} });
698
699     push( @query, "version" );
700     if ($version) {
701         push( @values, $version->name );
702     }
703     else {
704         my @versions = @{ $product->versions };
705         my $v        = $versions[0];
706         push( @values, $v->name );
707         $err .= "Unknown version \"";
708         $err .= ( defined $bug_fields{'version'} )
709             ? $bug_fields{'version'}
710             : "unknown";
711         $err .= " in product " . $product->name . ". \n";
712         $err .= "   Setting version to \"" . $v->name . "\".\n";
713     }
714
715     # Milestone
716     if ( $params->{"usetargetmilestone"} ) {
717         my $milestone;
718         if (defined $bug_fields{'target_milestone'}
719             && $bug_fields{'target_milestone'} ne "") {
720
721             $milestone = new Bugzilla::Milestone(
722                 { product => $product, name => $bug_fields{'target_milestone'} });
723         }
724         if ($milestone) {
725             push( @values, $milestone->name );
726         }
727         else {
728             push( @values, $product->default_milestone );
729             $err .= "Unknown milestone \"";
730             $err .= ( defined $bug_fields{'target_milestone'} )
731                 ? $bug_fields{'target_milestone'}
732                 : "unknown";
733             $err .= " in product " . $product->name . ". \n";
734             $err .= "   Setting to default milestone for this product, ";
735             $err .= "\"" . $product->default_milestone . "\".\n";
736         }
737         push( @query, "target_milestone" );
738     }
739
740     # For priority, severity, opsys and platform we check that the one being
741     # imported is valid. If it is not we use the defaults set in the parameters.
742     if (defined( $bug_fields{'bug_severity'} )
743         && check_field('bug_severity', scalar $bug_fields{'bug_severity'},
744                        undef, ERR_LEVEL) )
745     {
746         push( @values, $bug_fields{'bug_severity'} );
747     }
748     else {
749         push( @values, $params->{'defaultseverity'} );
750         $err .= "Unknown severity ";
751         $err .= ( defined $bug_fields{'bug_severity'} )
752           ? $bug_fields{'bug_severity'}
753           : "unknown";
754         $err .= ". Setting to default severity \"";
755         $err .= $params->{'defaultseverity'} . "\".\n";
756     }
757     push( @query, "bug_severity" );
758
759     if (defined( $bug_fields{'priority'} )
760         && check_field('priority', scalar $bug_fields{'priority'},
761                        undef, ERR_LEVEL ) )
762     {
763         push( @values, $bug_fields{'priority'} );
764     }
765     else {
766         push( @values, $params->{'defaultpriority'} );
767         $err .= "Unknown priority ";
768         $err .= ( defined $bug_fields{'priority'} )
769           ? $bug_fields{'priority'}
770           : "unknown";
771         $err .= ". Setting to default priority \"";
772         $err .= $params->{'defaultpriority'} . "\".\n";
773     }
774     push( @query, "priority" );
775
776     if (defined( $bug_fields{'rep_platform'} )
777         && check_field('rep_platform', scalar $bug_fields{'rep_platform'},
778                        undef, ERR_LEVEL ) )
779     {
780         push( @values, $bug_fields{'rep_platform'} );
781     }
782     else {
783         push( @values, $params->{'defaultplatform'} );
784         $err .= "Unknown platform ";
785         $err .= ( defined $bug_fields{'rep_platform'} )
786           ? $bug_fields{'rep_platform'}
787           : "unknown";
788         $err .=". Setting to default platform \"";
789         $err .= $params->{'defaultplatform'} . "\".\n";
790     }
791     push( @query, "rep_platform" );
792
793     if (defined( $bug_fields{'op_sys'} )
794         && check_field('op_sys',  scalar $bug_fields{'op_sys'},
795                        undef, ERR_LEVEL ) )
796     {
797         push( @values, $bug_fields{'op_sys'} );
798     }
799     else {
800         push( @values, $params->{'defaultopsys'} );
801         $err .= "Unknown operating system ";
802         $err .= ( defined $bug_fields{'op_sys'} )
803           ? $bug_fields{'op_sys'}
804           : "unknown";
805         $err .= ". Setting to default OS \"" . $params->{'defaultopsys'} . "\".\n";
806     }
807     push( @query, "op_sys" );
808
809     # Process time fields
810     if ( $params->{"timetrackinggroup"} ) {
811         my $date = format_time( $bug_fields{'deadline'}, "%Y-%m-%d" )
812           || undef;
813         push( @values, $date );
814         push( @query,  "deadline" );
815         if ( defined $bug_fields{'estimated_time'} ) {
816             eval {
817                 Bugzilla::Bug::ValidateTime($bug_fields{'estimated_time'}, "e");
818             };
819             if (!$@){
820                 push( @values, $bug_fields{'estimated_time'} );
821                 push( @query,  "estimated_time" );
822             }
823         }
824         if ( defined $bug_fields{'remaining_time'} ) {
825             eval {
826                 Bugzilla::Bug::ValidateTime($bug_fields{'remaining_time'}, "r");
827             };
828             if (!$@){
829                 push( @values, $bug_fields{'remaining_time'} );
830                 push( @query,  "remaining_time" );
831             }
832         }
833         if ( defined $bug_fields{'actual_time'} ) {
834             eval {
835                 Bugzilla::Bug::ValidateTime($bug_fields{'actual_time'}, "a");
836             };
837             if ($@){
838                 $bug_fields{'actual_time'} = 0.0;
839                 $err .= "Invalid Actual Time. Setting to 0.0\n";
840             }
841         }
842         else {
843             $bug_fields{'actual_time'} = 0.0;
844             $err .= "Actual time not defined. Setting to 0.0\n";
845         }
846     }
847
848     # Reporter Assignee QA Contact
849     my $exporterid = $exporter->id;
850     my $reporterid = login_to_id( $bug_fields{'reporter'} )
851       if $bug_fields{'reporter'};
852     push( @query, "reporter" );
853     if ( ( $bug_fields{'reporter'} ) && ($reporterid) ) {
854         push( @values, $reporterid );
855     }
856     else {
857         push( @values, $exporterid );
858         $err .= "The original reporter of this bug does not have\n";
859         $err .= "   an account here. Reassigning to the person who moved\n";
860         $err .= "   it here: $exporter_login.\n";
861         if ( $bug_fields{'reporter'} ) {
862             $err .= "   Previous reporter was $bug_fields{'reporter'}.\n";
863         }
864         else {
865             $err .= "   Previous reporter is unknown.\n";
866         }
867     }
868
869     my $changed_owner = 0;
870     my $owner;
871     push( @query, "assigned_to" );
872     if ( ( $bug_fields{'assigned_to'} )
873         && ( $owner = login_to_id( $bug_fields{'assigned_to'} )) ) {
874         push( @values, $owner );
875     }
876     else {
877         push( @values, $component->default_assignee->id );
878         $changed_owner = 1;
879         $err .= "The original assignee of this bug does not have\n";
880         $err .= "   an account here. Reassigning to the default assignee\n";
881         $err .= "   for the component, ". $component->default_assignee->login .".\n";
882         if ( $bug_fields{'assigned_to'} ) {
883             $err .= "   Previous assignee was $bug_fields{'assigned_to'}.\n";
884         }
885         else {
886             $err .= "   Previous assignee is unknown.\n";
887         }
888     }
889
890     if ( $params->{"useqacontact"} ) {
891         my $qa_contact;
892         push( @query, "qa_contact" );
893         if ( ( defined $bug_fields{'qa_contact'})
894             && ( $qa_contact = login_to_id( $bug_fields{'qa_contact'} ) ) ) {
895             push( @values, $qa_contact );
896         }
897         else {
898             push( @values, $component->default_qa_contact->id || undef );
899             if ($component->default_qa_contact->id){
900                 $err .= "Setting qa contact to the default for this product.\n";
901                 $err .= "   This bug either had no qa contact or an invalid one.\n";
902             }
903         }
904     }
905
906     # Status & Resolution
907     my $has_res = defined($bug_fields{'resolution'});
908     my $has_status = defined($bug_fields{'bug_status'});
909     my $valid_res = check_field('resolution',  
910                                   scalar $bug_fields{'resolution'}, 
911                                   undef, ERR_LEVEL );
912     my $valid_status = check_field('bug_status',  
913                                   scalar $bug_fields{'bug_status'}, 
914                                   undef, ERR_LEVEL );
915     my $is_open = is_open_state($bug_fields{'bug_status'}); 
916     my $status = $bug_fields{'bug_status'} || undef;
917     my $resolution = $bug_fields{'resolution'} || undef;
918     
919     # Check everconfirmed 
920     my $everconfirmed;
921     if ($product->votes_to_confirm) {
922         $everconfirmed = $bug_fields{'everconfirmed'} || 0;
923     }
924     else {
925         $everconfirmed = 1;
926     }
927     push (@query,  "everconfirmed");
928     push (@values, $everconfirmed);
929
930     # Sanity check will complain about having bugs marked duplicate but no
931     # entry in the dup table. Since we can't tell the bug ID of bugs
932     # that might not yet be in the database we have no way of populating
933     # this table. Change the resolution instead.
934     if ( $valid_res  && ( $bug_fields{'resolution'} eq "DUPLICATE" ) ) {
935         $resolution = "MOVED";
936         $err .= "This bug was marked DUPLICATE in the database ";
937         $err .= "it was moved from.\n    Changing resolution to \"MOVED\"\n";
938     } 
939
940     # If there is at least 1 initial bug status different from UNCO, use it,
941     # else use the open bug status with the lowest sortkey (different from UNCO).
942     my @bug_statuses = @{Bugzilla::Status->can_change_to()};
943     @bug_statuses = grep { $_->name ne 'UNCONFIRMED' } @bug_statuses;
944
945     my $initial_status;
946     if (scalar(@bug_statuses)) {
947         $initial_status = $bug_statuses[0]->name;
948     }
949     else {
950         @bug_statuses = @{Bugzilla::Status->get_all()};
951         # Exclude UNCO and inactive bug statuses.
952         @bug_statuses = grep { $_->is_active && $_->name ne 'UNCONFIRMED'} @bug_statuses;
953         my @open_statuses = grep { $_->is_open } @bug_statuses;
954         if (scalar(@open_statuses)) {
955             $initial_status = $open_statuses[0]->name;
956         }
957         else {
958             # There is NO other open bug statuses outside UNCO???
959             Error("no open bug statuses available.");
960         }
961     }
962
963     if($has_status){
964         if($valid_status){
965             if($is_open){
966                 if($has_res){
967                     $err .= "Resolution set on an open status.\n";
968                     $err .= "   Dropping resolution $resolution\n";
969                     $resolution = undef;
970                 }
971                 if($changed_owner){
972                     if($everconfirmed){  
973                         $status = $initial_status;
974                     }
975                     else{
976                         $status = "UNCONFIRMED";
977                     }
978                     if ($status ne $bug_fields{'bug_status'}){
979                         $err .= "Bug reassigned, setting status to \"$status\".\n";
980                         $err .= "   Previous status was \"";
981                         $err .=  $bug_fields{'bug_status'} . "\".\n";
982                     }
983                 }
984                 if($everconfirmed){
985                     if($status eq "UNCONFIRMED"){
986                         $err .= "Bug Status was UNCONFIRMED but everconfirmed was true\n";
987                         $err .= "   Setting status to $initial_status\n";
988                         $err .= "Resetting votes to 0\n" if ( $bug_fields{'votes'} );
989                         $status = $initial_status;
990                     }
991                 }
992                 else{ # $everconfirmed is false
993                     if($status ne "UNCONFIRMED"){
994                         $err .= "Bug Status was $status but everconfirmed was false\n";
995                         $err .= "   Setting status to UNCONFIRMED\n";
996                         $status = "UNCONFIRMED";
997                     }
998                 }
999             }
1000             else{ # $is_open is false
1001                if(!$has_res){
1002                    $err .= "Missing Resolution. Setting status to ";
1003                    if($everconfirmed){
1004                        $status = $initial_status;
1005                        $err .= "$initial_status\n";
1006                    }
1007                    else{
1008                        $status = "UNCONFIRMED";
1009                        $err .= "UNCONFIRMED\n";
1010                    }
1011                }
1012                if(!$valid_res){
1013                    $err .= "Unknown resolution \"$resolution\".\n";
1014                    $err .= "   Setting resolution to MOVED\n";
1015                    $resolution = "MOVED";
1016                }
1017             }   
1018         }
1019         else{ # $valid_status is false
1020             if($everconfirmed){  
1021                 $status = $initial_status;
1022             }
1023             else{
1024                 $status = "UNCONFIRMED";
1025             }        
1026             $err .= "Bug has invalid status, setting status to \"$status\".\n";
1027             $err .= "   Previous status was \"";
1028             $err .=  $bug_fields{'bug_status'} . "\".\n";
1029             $resolution = undef;
1030         }
1031                 
1032     }
1033     else{ #has_status is false
1034         if($everconfirmed){  
1035             $status = $initial_status;
1036         }
1037         else{
1038             $status = "UNCONFIRMED";
1039         }        
1040         $err .= "Bug has no status, setting status to \"$status\".\n";
1041         $err .= "   Previous status was unknown\n";
1042         $resolution = undef;
1043     }
1044                                  
1045     if (defined $resolution){
1046         push( @query,  "resolution" );
1047         push( @values, $resolution );
1048     }
1049     
1050     # Bug status
1051     push( @query,  "bug_status" );
1052     push( @values, $status );
1053
1054     # Custom fields - Multi-select fields have their own table.
1055     my %multi_select_fields;
1056     foreach my $field (Bugzilla->active_custom_fields) {
1057         my $custom_field = $field->name;
1058         my $value = $bug_fields{$custom_field};
1059         next unless defined $value;
1060         if ($field->type == FIELD_TYPE_FREETEXT) {
1061             push(@query, $custom_field);
1062             push(@values, clean_text($value));
1063         } elsif ($field->type == FIELD_TYPE_TEXTAREA) {
1064             push(@query, $custom_field);
1065             push(@values, $value);
1066         } elsif ($field->type == FIELD_TYPE_SINGLE_SELECT) {
1067             my $is_well_formed = check_field($custom_field, $value, undef, ERR_LEVEL);
1068             if ($is_well_formed) {
1069                 push(@query, $custom_field);
1070                 push(@values, $value);
1071             } else {
1072                 $err .= "Skipping illegal value \"$value\" in $custom_field.\n" ;
1073             }
1074         } elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
1075             my @legal_values;
1076             foreach my $item (_to_array($value)) {
1077                 my $is_well_formed = check_field($custom_field, $item, undef, ERR_LEVEL);
1078                 if ($is_well_formed) {
1079                     push(@legal_values, $item);
1080                 } else {
1081                     $err .= "Skipping illegal value \"$item\" in $custom_field.\n" ;
1082                 }
1083             }
1084             if (scalar @legal_values) {
1085                 $multi_select_fields{$custom_field} = \@legal_values;
1086             }
1087         } elsif ($field->type == FIELD_TYPE_DATETIME) {
1088             eval { $value = Bugzilla::Bug->_check_datetime_field($value); };
1089             if ($@) {
1090                 $err .= "Skipping illegal value \"$value\" in $custom_field.\n" ;
1091             }
1092             else {
1093                 push(@query, $custom_field);
1094                 push(@values, $value);
1095             }
1096         } else {
1097             $err .= "Type of custom field $custom_field is an unhandled FIELD_TYPE: " .
1098                     $field->type . "\n";
1099         }
1100     }
1101
1102     # For the sake of sanitycheck.cgi we do this.
1103     # Update lastdiffed if you do not want to have mail sent
1104     unless ($mail) {
1105         push @query,  "lastdiffed";
1106         push @values, $timestamp;
1107     }
1108
1109     # INSERT the bug
1110     my $query = "INSERT INTO bugs (" . join( ", ", @query ) . ") VALUES (";
1111        $query .= '?,' foreach (@values);
1112     chop($query);    # Remove the last comma.
1113        $query .= ")";
1114
1115     $dbh->do( $query, undef, @values );
1116     my $id = $dbh->bz_last_key( 'bugs', 'bug_id' );
1117
1118     # We are almost certain to get some uninitialized warnings
1119     # Since this is just for debugging the query, let's shut them up
1120     eval {
1121         no warnings 'uninitialized';
1122         Debug(
1123             "Bug Query: INSERT INTO bugs (\n"
1124               . join( ",\n", @query )
1125               . "\n) VALUES (\n"
1126               . join( ",\n", @values ),
1127             DEBUG_LEVEL
1128         );
1129     };
1130
1131     # Handle CC's
1132     if ( defined $bug_fields{'cc'} ) {
1133         my %ccseen;
1134         my $sth_cc = $dbh->prepare("INSERT INTO cc (bug_id, who) VALUES (?,?)");
1135         foreach my $person (_to_array($bug_fields{'cc'})) {
1136             next unless $person;
1137             my $uid;
1138             if ($uid = login_to_id($person)) {
1139                 if ( !$ccseen{$uid} ) {
1140                     $sth_cc->execute( $id, $uid );
1141                     $ccseen{$uid} = 1;
1142                 }
1143             }
1144             else {
1145                 $err .= "CC member $person does not have an account here\n";
1146             }
1147         }
1148     }
1149
1150     # Handle keywords
1151     if ( defined( $bug_fields{'keywords'} ) ) {
1152         my %keywordseen;
1153         my $key_sth = $dbh->prepare(
1154             "INSERT INTO keywords 
1155                       (bug_id, keywordid) VALUES (?,?)"
1156         );
1157         foreach my $keyword ( split( /[\s,]+/, $bug_fields{'keywords'} )) {
1158             next unless $keyword;
1159             my $keyword_obj = new Bugzilla::Keyword({name => $keyword});
1160             if (!$keyword_obj) {
1161                 $err .= "Skipping unknown keyword: $keyword.\n";
1162                 next;
1163             }
1164             if (!$keywordseen{$keyword_obj->id}) {
1165                 $key_sth->execute($id, $keyword_obj->id);
1166                 $keywordseen{$keyword_obj->id} = 1;
1167             }
1168         }
1169         my ($keywordarray) = $dbh->selectcol_arrayref(
1170             "SELECT d.name FROM keyworddefs d
1171                     INNER JOIN keywords k 
1172                     ON d.id = k.keywordid 
1173                     WHERE k.bug_id = ? 
1174                     ORDER BY d.name", undef, $id);
1175         my $keywordstring = join( ", ", @{$keywordarray} );
1176         $dbh->do( "UPDATE bugs SET keywords = ? WHERE bug_id = ?",
1177             undef, $keywordstring, $id )
1178     }
1179
1180     # Insert values of custom multi-select fields. They have already
1181     # been validated.
1182     foreach my $custom_field (keys %multi_select_fields) {
1183         my $sth = $dbh->prepare("INSERT INTO bug_$custom_field
1184                                  (bug_id, value) VALUES (?, ?)");
1185         foreach my $value (@{$multi_select_fields{$custom_field}}) {
1186             $sth->execute($id, $value);
1187         }
1188     }
1189
1190     # Parse bug flags
1191     foreach my $bflag ( $bug->children('flag')) {
1192         next unless ( defined($bflag) );
1193         $err .= flag_handler(
1194             $bflag->{'att'}->{'name'},   $bflag->{'att'}->{'status'},
1195             $bflag->{'att'}->{'setter'}, $bflag->{'att'}->{'requestee'},
1196             $exporterid,                 $id,
1197             $comp_id,                    $prod_id,
1198             undef
1199         );
1200     }
1201
1202     # Insert Attachments for the bug
1203     foreach my $att (@attachments) {
1204         if ($att eq "err"){
1205             $err .= "No attachment ID specified, dropping attachment\n";
1206             next;
1207         }
1208         if (!$exporter->in_group($params->{'insidergroup'}) && $att->{'isprivate'}){
1209             $err .= "Exporter not in insidergroup and attachment marked private.\n";
1210             $err .= "   Marking attachment public\n";
1211             $att->{'isprivate'} = 0;
1212         }
1213
1214         my $attacher_id = $att->{'attacher'} ? login_to_id($att->{'attacher'}) : undef;
1215
1216         $dbh->do("INSERT INTO attachments 
1217                  (bug_id, creation_ts, modification_time, filename, description,
1218                  mimetype, ispatch, isprivate, isobsolete, submitter_id) 
1219                  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1220             undef, $id, $att->{'date'}, $att->{'date'}, $att->{'filename'},
1221             $att->{'desc'}, $att->{'ctype'}, $att->{'ispatch'},
1222             $att->{'isprivate'}, $att->{'isobsolete'}, $attacher_id || $exporterid);
1223         my $att_id   = $dbh->bz_last_key( 'attachments', 'attach_id' );
1224         my $att_data = $att->{'data'};
1225         my $sth = $dbh->prepare("INSERT INTO attach_data (id, thedata) 
1226                                  VALUES ($att_id, ?)" );
1227         trick_taint($att_data);
1228         $sth->bind_param( 1, $att_data, $dbh->BLOB_TYPE );
1229         $sth->execute();
1230
1231         $comments .= "Imported an attachment (id=$att_id)\n";
1232         if (!$attacher_id) {
1233             if ($att->{'attacher'}) {
1234                 $err .= "The original submitter of attachment $att_id was\n   ";
1235                 $err .= $att->{'attacher'} . ", but he doesn't have an account here.\n";
1236             }
1237             else {
1238                 $err .= "The original submitter of attachment $att_id is unknown.\n";
1239             }
1240             $err .= "   Reassigning to the person who moved it here: $exporter_login.\n";
1241         }
1242
1243         # Process attachment flags
1244         foreach my $aflag (@{ $att->{'flags'} }) {
1245             next unless defined($aflag) ;
1246             $err .= flag_handler(
1247                 $aflag->{'name'},   $aflag->{'status'},
1248                 $aflag->{'setter'}, $aflag->{'requestee'},
1249                 $exporterid,        $id,
1250                 $comp_id,           $prod_id,
1251                 $att_id
1252             );
1253         }
1254     }
1255
1256     # Clear the attachments array for the next bug
1257     @attachments = ();
1258
1259     # Insert longdesc and append any errors
1260     my $worktime = $bug_fields{'actual_time'} || 0.0;
1261     $worktime = 0.0 if (!$exporter->in_group($params->{'timetrackinggroup'}));
1262     $long_description .= "\n" . $comments;
1263     if ($err) {
1264         $long_description .= "\n$err\n";
1265     }
1266     trick_taint($long_description);
1267     $dbh->do("INSERT INTO longdescs 
1268                      (bug_id, who, bug_when, work_time, isprivate, thetext) 
1269                      VALUES (?,?,?,?,?,?)", undef,
1270         $id, $exporterid, $timestamp, $worktime, $private, $long_description
1271     );
1272     Bugzilla::Bug->new($id)->_sync_fulltext('new_bug');
1273
1274     # Add this bug to each group of which its product is a member.
1275     my $sth_group = $dbh->prepare("INSERT INTO bug_group_map (bug_id, group_id) 
1276                          VALUES (?, ?)");
1277     foreach my $group_id ( keys %{ $product->group_controls } ) {
1278         if ($product->group_controls->{$group_id}->{'membercontrol'} != CONTROLMAPNA
1279             && $product->group_controls->{$group_id}->{'othercontrol'} != CONTROLMAPNA){
1280             $sth_group->execute( $id, $group_id );
1281         }
1282     }
1283
1284     $log .= "Bug ${urlbase}show_bug.cgi?id=$bug_fields{'bug_id'} ";
1285     $log .= "imported as bug $id.\n";
1286     $log .= $params->{"urlbase"} . "show_bug.cgi?id=$id\n\n";
1287     if ($err) {
1288         $log .= "The following problems were encountered while creating bug $id.\n";
1289         $log .= $err;
1290         $log .= "You may have to set certain fields in the new bug by hand.\n\n";
1291     }
1292     Debug( $log, OK_LEVEL );
1293     push(@logs, $log);
1294     Bugzilla::BugMail::Send( $id, { 'changer' => $exporter_login } ) if ($mail);
1295
1296     # done with the xml data. Lets clear it from memory
1297     $twig->purge;
1298
1299 }
1300
1301 Debug( "Reading xml", DEBUG_LEVEL );
1302
1303 # Read STDIN in slurp mode. VERY dangerous, but we live on the wild side ;-)
1304 local ($/);
1305 $xml = <>;
1306
1307 # If there's anything except whitespace before <?xml then we guess it's a mail
1308 # and MIME::Parser should parse it. Else don't.
1309 if ($xml =~ m/\S.*<\?xml/s ) {
1310
1311     # If the email was encoded (Mailer::MessageToMTA() does it when using UTF-8),
1312     # we have to decode it first, else the XML parsing will fail.
1313     my $parser = MIME::Parser->new;
1314     $parser->output_to_core(1);
1315     $parser->tmp_to_core(1);
1316     my $entity = $parser->parse_data($xml);
1317     my $bodyhandle = $entity->bodyhandle;
1318     $xml = $bodyhandle->as_string;
1319
1320 }
1321
1322 # remove everything in file before xml header
1323 $xml =~ s/^.+(<\?xml version.+)$/$1/s;
1324
1325 Debug( "Parsing tree", DEBUG_LEVEL );
1326 my $twig = XML::Twig->new(
1327     twig_handlers => {
1328         bug        => \&process_bug,
1329         attachment => \&process_attachment
1330     },
1331     start_tag_handlers => { bugzilla => \&init }
1332 );
1333 $twig->parse($xml);
1334 my $root       = $twig->root;
1335 my $maintainer = $root->{'att'}->{'maintainer'};
1336 my $exporter   = $root->{'att'}->{'exporter'};
1337 my $urlbase    = $root->{'att'}->{'urlbase'};
1338
1339 # It is time to email the result of the import.
1340 my $log = join("\n\n", @logs);
1341 $log .=  "\n\nImported $bugtotal bug(s) from $urlbase,\n  sent by $exporter.\n";
1342 my $subject =  "$bugtotal Bug(s) successfully moved from $urlbase to " 
1343    . $params->{"urlbase"};
1344 my @to = ($exporter, $maintainer);
1345 MailMessage( $subject, $log, @to );
1346
1347 __END__
1348
1349 =head1 NAME
1350
1351 importxml - Import bugzilla bug data from xml.
1352
1353 =head1 SYNOPSIS
1354
1355     importxml.pl [options] [file ...]
1356
1357  Options:
1358        -? --help        brief help message
1359        -v --verbose     print error and debug information. 
1360                         Multiple -v increases verbosity
1361        -m --sendmail    send mail to recipients with log of bugs imported
1362        --attach_path    The path to the attachment files.
1363                         (Required if encoding="filename" is used for attachments.)
1364
1365 =head1 OPTIONS
1366
1367 =over 8
1368
1369 =item B<-?>
1370
1371     Print a brief help message and exits.
1372
1373 =item B<-v>
1374
1375     Print error and debug information. Mulltiple -v increases verbosity
1376
1377 =item B<-m>
1378
1379     Send mail to exporter with a log of bugs imported and any errors.
1380
1381 =back
1382
1383 =head1 DESCRIPTION
1384
1385      This script is used to import bugs from another installation of bugzilla.
1386      It can be used in two ways.
1387      First using the move function of bugzilla
1388      on another system will send mail to an alias provided by
1389      the administrator of the target installation (you). Set up an alias
1390      similar to the one given below so this mail will be automatically 
1391      run by this script and imported into your database.  Run 'newaliases'
1392      after adding this alias to your aliases file. Make sure your sendmail
1393      installation is configured to allow mail aliases to execute code. 
1394
1395      bugzilla-import: "|/usr/bin/perl /opt/bugzilla/importxml.pl --mail"
1396
1397      Second it can be run from the command line with any xml file from 
1398      STDIN that conforms to the bugzilla DTD. In this case you can pass 
1399      an argument to set whether you want to send the
1400      mail that will be sent to the exporter and maintainer normally.
1401
1402      importxml.pl [options] bugsfile.xml
1403
1404 =cut
1405