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