Rubber-stamped by Brady Eidson.
[webbrowser.git] / BugsSite / importxml.pl
blob6309b4e9146fbee30d610164b7cdb4c168556783
1 #!/usr/bin/env perl -wT
2 # -*- Mode: perl; indent-tabs-mode: nil -*-
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/
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.
14 # The Original Code is the Bugzilla Bug Tracking System.
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.
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>
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.
32 use strict;
34 #####################################################################
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.
46 # bugzilla-import: "|/usr/bin/perl /opt/bugzilla/importxml.pl"
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.
53 # importxml.pl bugsfile.xml
55 #####################################################################
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));
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;
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;
88 use MIME::Base64;
89 use MIME::Parser;
90 use Date::Format;
91 use Getopt::Long;
92 use Pod::Usage;
93 use XML::Twig;
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);
99 my $debug = 0;
100 my $mail = '';
101 my $attach_path = '';
102 my $help = 0;
104 my $result = GetOptions(
105 "verbose|debug+" => \$debug,
106 "mail|sendmail!" => \$mail,
107 "attach_path=s" => \$attach_path,
108 "help|?" => \$help
111 pod2usage(0) if $help;
113 use constant OK_LEVEL => 3;
114 use constant DEBUG_LEVEL => 2;
115 use constant ERR_LEVEL => 1;
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()");
125 ###############################################################################
126 # Helper sub routines #
127 ###############################################################################
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;
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);
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 ) );
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;
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
176 = @_;
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;
184 unless ($setter) {
185 $err = "Invalid setter $setter_login on $type flag $name\n";
186 $err .= " Dropping flag $name\n";
187 return $err;
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;
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";
202 else{
203 $requestee_id = $requestee->id;
206 else {
207 $err = "Invalid requestee $requestee_login on $type flag $name\n";
208 $err .= " Requesting from the wind.\n";
212 my $flag_types;
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(
219 'target_type' => 'attachment',
220 'product_id' => $productid,
221 'component_id' => $componentid
222 } );
224 else {
225 my $bug = new Bugzilla::Bug($bugid);
226 $flag_types = $bug->flag_types;
228 unless ($flag_types){
229 $err = "No flag types defined for this bug\n";
230 $err .= " Dropping flag $name\n";
231 return $err;
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;
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;
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;
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;
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));
277 else {
278 $err = "Dropping unknown $type flag: $name\n";
279 return $err;
281 return $err;
284 # Converts and returns the input data as an array.
285 sub _to_array {
286 my $value = shift;
288 $value = [$value] if !ref($value);
289 return @$value;
292 ###############################################################################
293 # XML Handlers #
294 ###############################################################################
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.
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
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'};
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);
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(
334 product => $def_product,
335 name => $params->{"moved-default-component"}
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);
343 # Parse attachments.
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>
354 # The attach_id is ignored since mysql generates a new one for us.
355 # The submitter_id gets filled in with $exporterid.
357 sub process_attachment() {
358 my ( $twig, $attach ) = @_;
359 Debug( "Parsing attachments", DEBUG_LEVEL );
360 my %attachment;
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;
382 elsif ($encoding =~ /filename/) {
383 # read the attachment file
384 Error("attach_path is required", undef) unless ($attach_path);
386 my $filename = $attach->field('data');
387 # Remove any leading path data from the filename
388 $filename =~ s/(.*\/|.*\\)//gs;
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;
397 else {
398 $attachment{'data'} = $attach->field('data');
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;
411 $attachment{'flags'} = \@aflags if (@aflags);
413 # free up the memory for use by the rest of the script
414 $attach->delete;
415 if ($attachment{'attachid'}) {
416 push @attachments, \%attachment;
418 else {
419 push @attachments, "err";
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.
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'};
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";
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";
454 return;
456 $bugtotal++;
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() )
464 $all_fields{$field} = 1;
467 my %bug_fields;
468 my $err = "";
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 );
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';
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;
485 else {
486 $bug_fields{$bugchild->name} = $values[0];
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";
501 else {
502 $err .= $bugchild->field;
504 $err .= "</" . $bugchild->name . ">\n";
508 my @long_descs;
509 my $private = 0;
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;
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;
523 my $data = $comment->field('thetext');
524 if ( defined $comment->first_child('thetext')->{'att'}->{'encoding'}
525 && $comment->first_child('thetext')->{'att'}->{'encoding'} =~
526 /base64/ )
528 $data = decode_base64($data);
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;
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;
543 $long_desc{'thetext'} = $data;
544 push @long_descs, \%long_desc;
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 ";
561 else {
562 $long_description .= "\n\n\n---- Additional Comments From ";
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";
574 my $comments;
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";
586 if ( defined $bug_fields{'blocked'} ) {
587 $comments .= "This bug blocked bug(s) " .
588 join(' ', _to_array($bug_fields{'blocked'})) . ".\n";
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 = ();
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} );
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";
616 else {
617 $alias = $bug_fields{'alias'};
618 push @query, 'alias';
619 push @values, $alias;
623 # Timestamps
624 push( @query, "creation_ts" );
625 push( @values,
626 format_time( $bug_fields{'creation_ts'}, "%Y-%m-%d %X" )
627 || $timestamp );
629 push( @query, "delta_ts" );
630 push( @values,
631 format_time( $bug_fields{'delta_ts'}, "%Y-%m-%d %X" )
632 || $timestamp );
634 # Bug Access
635 push( @query, "cclist_accessible" );
636 push( @values, $bug_fields{'cclist_accessible'} ? 1 : 0 );
638 push( @query, "reporter_accessible" );
639 push( @values, $bug_fields{'reporter_accessible'} ? 1 : 0 );
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(
647 product => $def_product,
648 name => $params->{"moved-default-component"}
651 my $product;
652 my $component;
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";
662 else {
663 $product = $def_product;
665 if ( defined $bug_fields{'component'} ) {
666 $component = new Bugzilla::Component(
668 product => $product,
669 name => $bug_fields{'component'}
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";
680 else {
681 $component = $def_component;
682 $product = $def_product;
685 my $prod_id = $product->id;
686 my $comp_id = $component->id;
688 push( @query, "product_id" );
689 push( @values, $prod_id );
690 push( @query, "component_id" );
691 push( @values, $comp_id );
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'} });
699 push( @query, "version" );
700 if ($version) {
701 push( @values, $version->name );
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";
715 # Milestone
716 if ( $params->{"usetargetmilestone"} ) {
717 my $milestone;
718 if (defined $bug_fields{'target_milestone'}
719 && $bug_fields{'target_milestone'} ne "") {
721 $milestone = new Bugzilla::Milestone(
722 { product => $product, name => $bug_fields{'target_milestone'} });
724 if ($milestone) {
725 push( @values, $milestone->name );
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";
737 push( @query, "target_milestone" );
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) )
746 push( @values, $bug_fields{'bug_severity'} );
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";
757 push( @query, "bug_severity" );
759 if (defined( $bug_fields{'priority'} )
760 && check_field('priority', scalar $bug_fields{'priority'},
761 undef, ERR_LEVEL ) )
763 push( @values, $bug_fields{'priority'} );
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";
774 push( @query, "priority" );
776 if (defined( $bug_fields{'rep_platform'} )
777 && check_field('rep_platform', scalar $bug_fields{'rep_platform'},
778 undef, ERR_LEVEL ) )
780 push( @values, $bug_fields{'rep_platform'} );
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";
791 push( @query, "rep_platform" );
793 if (defined( $bug_fields{'op_sys'} )
794 && check_field('op_sys', scalar $bug_fields{'op_sys'},
795 undef, ERR_LEVEL ) )
797 push( @values, $bug_fields{'op_sys'} );
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";
807 push( @query, "op_sys" );
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");
819 if (!$@){
820 push( @values, $bug_fields{'estimated_time'} );
821 push( @query, "estimated_time" );
824 if ( defined $bug_fields{'remaining_time'} ) {
825 eval {
826 Bugzilla::Bug::ValidateTime($bug_fields{'remaining_time'}, "r");
828 if (!$@){
829 push( @values, $bug_fields{'remaining_time'} );
830 push( @query, "remaining_time" );
833 if ( defined $bug_fields{'actual_time'} ) {
834 eval {
835 Bugzilla::Bug::ValidateTime($bug_fields{'actual_time'}, "a");
837 if ($@){
838 $bug_fields{'actual_time'} = 0.0;
839 $err .= "Invalid Actual Time. Setting to 0.0\n";
842 else {
843 $bug_fields{'actual_time'} = 0.0;
844 $err .= "Actual time not defined. Setting to 0.0\n";
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 );
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";
864 else {
865 $err .= " Previous reporter is unknown.\n";
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 );
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";
885 else {
886 $err .= " Previous assignee is unknown.\n";
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 );
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";
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;
919 # Check everconfirmed
920 my $everconfirmed;
921 if ($product->votes_to_confirm) {
922 $everconfirmed = $bug_fields{'everconfirmed'} || 0;
924 else {
925 $everconfirmed = 1;
927 push (@query, "everconfirmed");
928 push (@values, $everconfirmed);
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";
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;
945 my $initial_status;
946 if (scalar(@bug_statuses)) {
947 $initial_status = $bug_statuses[0]->name;
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;
957 else {
958 # There is NO other open bug statuses outside UNCO???
959 Error("no open bug statuses available.");
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;
971 if($changed_owner){
972 if($everconfirmed){
973 $status = $initial_status;
975 else{
976 $status = "UNCONFIRMED";
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";
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;
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";
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";
1007 else{
1008 $status = "UNCONFIRMED";
1009 $err .= "UNCONFIRMED\n";
1012 if(!$valid_res){
1013 $err .= "Unknown resolution \"$resolution\".\n";
1014 $err .= " Setting resolution to MOVED\n";
1015 $resolution = "MOVED";
1019 else{ # $valid_status is false
1020 if($everconfirmed){
1021 $status = $initial_status;
1023 else{
1024 $status = "UNCONFIRMED";
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;
1033 else{ #has_status is false
1034 if($everconfirmed){
1035 $status = $initial_status;
1037 else{
1038 $status = "UNCONFIRMED";
1040 $err .= "Bug has no status, setting status to \"$status\".\n";
1041 $err .= " Previous status was unknown\n";
1042 $resolution = undef;
1045 if (defined $resolution){
1046 push( @query, "resolution" );
1047 push( @values, $resolution );
1050 # Bug status
1051 push( @query, "bug_status" );
1052 push( @values, $status );
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" ;
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" ;
1084 if (scalar @legal_values) {
1085 $multi_select_fields{$custom_field} = \@legal_values;
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" ;
1092 else {
1093 push(@query, $custom_field);
1094 push(@values, $value);
1096 } else {
1097 $err .= "Type of custom field $custom_field is an unhandled FIELD_TYPE: " .
1098 $field->type . "\n";
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;
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 .= ")";
1115 $dbh->do( $query, undef, @values );
1116 my $id = $dbh->bz_last_key( 'bugs', 'bug_id' );
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
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;
1144 else {
1145 $err .= "CC member $person does not have an account here\n";
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 (?,?)"
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;
1164 if (!$keywordseen{$keyword_obj->id}) {
1165 $key_sth->execute($id, $keyword_obj->id);
1166 $keywordseen{$keyword_obj->id} = 1;
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 )
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);
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
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;
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;
1214 my $attacher_id = $att->{'attacher'} ? login_to_id($att->{'attacher'}) : undef;
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();
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";
1237 else {
1238 $err .= "The original submitter of attachment $att_id is unknown.\n";
1240 $err .= " Reassigning to the person who moved it here: $exporter_login.\n";
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
1256 # Clear the attachments array for the next bug
1257 @attachments = ();
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";
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
1272 Bugzilla::Bug->new($id)->_sync_fulltext('new_bug');
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 );
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";
1292 Debug( $log, OK_LEVEL );
1293 push(@logs, $log);
1294 Bugzilla::BugMail::Send( $id, { 'changer' => $exporter_login } ) if ($mail);
1296 # done with the xml data. Lets clear it from memory
1297 $twig->purge;
1301 Debug( "Reading xml", DEBUG_LEVEL );
1303 # Read STDIN in slurp mode. VERY dangerous, but we live on the wild side ;-)
1304 local ($/);
1305 $xml = <>;
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 ) {
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;
1322 # remove everything in file before xml header
1323 $xml =~ s/^.+(<\?xml version.+)$/$1/s;
1325 Debug( "Parsing tree", DEBUG_LEVEL );
1326 my $twig = XML::Twig->new(
1327 twig_handlers => {
1328 bug => \&process_bug,
1329 attachment => \&process_attachment
1331 start_tag_handlers => { bugzilla => \&init }
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'};
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 );
1347 __END__
1349 =head1 NAME
1351 importxml - Import bugzilla bug data from xml.
1353 =head1 SYNOPSIS
1355 importxml.pl [options] [file ...]
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.)
1365 =head1 OPTIONS
1367 =over 8
1369 =item B<-?>
1371 Print a brief help message and exits.
1373 =item B<-v>
1375 Print error and debug information. Mulltiple -v increases verbosity
1377 =item B<-m>
1379 Send mail to exporter with a log of bugs imported and any errors.
1381 =back
1383 =head1 DESCRIPTION
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.
1395 bugzilla-import: "|/usr/bin/perl /opt/bugzilla/importxml.pl --mail"
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.
1402 importxml.pl [options] bugsfile.xml
1404 =cut