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
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.
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.
61 require File
::Basename
;
62 my $dir = $0; $dir =~ /(.*)/; $dir = $1; # trick taint
63 chdir(File
::Basename
::dirname
($dir));
67 # Data dumber is used for debugging, I got tired of copying it back in
68 # and then removing it.
74 use Bugzilla
::Product
;
75 use Bugzilla
::Version
;
76 use Bugzilla
::Component
;
77 use Bugzilla
::Milestone
;
78 use Bugzilla
::FlagType
;
79 use Bugzilla
::BugMail
;
83 use Bugzilla
::Constants
;
84 use Bugzilla
::Keyword
;
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
);
101 my $attach_path = '';
104 my $result = GetOptions
(
105 "verbose|debug+" => \
$debug,
106 "mail|sendmail!" => \
$mail,
107 "attach_path=s" => \
$attach_path,
111 pod2usage
(0) if $help;
113 use constant OK_LEVEL
=> 3;
114 use constant DEBUG_LEVEL
=> 2;
115 use constant ERR_LEVEL
=> 1;
121 my $dbh = Bugzilla
->dbh;
122 my $params = Bugzilla
->params;
123 my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
125 ###############################################################################
126 # Helper sub routines #
127 ###############################################################################
130 return unless ($mail);
134 my $from = $params->{"moved-from-address"};
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);
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
) );
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 );
168 # This subroutine handles flags for process_bug. It is generic in that
169 # it can handle both attachment flags and bug flags.
172 $name, $status, $setter_login,
173 $requestee_login, $exporterid, $bugid,
174 $productid, $componentid, $attachid
178 my $type = ($attachid) ?
"attachment" : "bug";
180 my $setter = new Bugzilla
::User
({ name
=> $setter_login });
185 $err = "Invalid setter $setter_login on $type flag $name\n";
186 $err .= " Dropping flag $name\n";
189 if ( !$setter->can_see_bug($bugid) ) {
190 $err .= "Setter is not a member of bug group\n";
191 $err .= " Dropping flag $name\n";
194 my $setter_id = $setter->id;
195 if ( defined($requestee_login) ) {
196 $requestee = new Bugzilla
::User
({ name
=> $requestee_login });
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";
203 $requestee_id = $requestee->id;
207 $err = "Invalid requestee $requestee_login on $type flag $name\n";
208 $err .= " Requesting from the wind.\n";
214 # If this is an attachment flag we need to do some dirty work to look
217 $flag_types = Bugzilla
::FlagType
::match
(
219 'target_type' => 'attachment',
220 'product_id' => $productid,
221 'component_id' => $componentid
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";
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.
238 foreach my $f ( @
{$flag_types} ) {
239 if ( $f->name eq $name) {
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";
254 my $request_group = $ftype->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";
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";
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));
278 $err = "Dropping unknown $type flag: $name\n";
284 # Converts and returns the input data as an array.
288 $value = [$value] if !ref($value);
292 ###############################################################################
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
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
);
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);
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);
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
);
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/) {
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
> };
398 $attachment{'data'} = $attach->field('data');
403 foreach my $aflag ( $attach->children('flag') ) {
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
415 if ($attachment{'attachid'}) {
416 push @attachments, \
%attachment;
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
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.
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.
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";
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.
462 qw(long_desc attachment flag group), Bugzilla
::Bug
::fields
() )
464 $all_fields{$field} = 1;
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;
486 $bug_fields{$bugchild->name} = $values[0];
490 $err .= "Unknown bug field \"" . $bugchild->name . "\"";
491 $err .= " encountered while moving bug\n";
492 $err .= " <" . $bugchild->name . ">";
493 if ( $bugchild->children_count > 1 ) {
495 foreach my $subchild ( $bugchild->children() ) {
496 $err .= " <" . $subchild->name . ">";
497 $err .= $subchild->field;
498 $err .= "</" . $subchild->name . ">\n";
502 $err .= $bugchild->field;
504 $err .= "</" . $bugchild->name . ">\n";
511 # Parse long descriptions
512 foreach my $comment ( $bug->children('long_desc') ) {
513 Debug
( "Parsing Long Description", DEBUG_LEVEL
);
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'})) {
523 my $data = $comment->field('thetext');
524 if ( defined $comment->first_child('thetext')->{'att'}->{'encoding'}
525 && $comment->first_child('thetext')->{'att'}->{'encoding'} =~
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
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
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++ ) {
559 $long_description .= "\n\n\n---- Reported by ";
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";
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
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} );
608 if ( $bug_fields{'alias'} ) {
609 my ($alias) = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs
610 WHERE alias = ?", undef,
611 $bug_fields{'alias'} );
613 $err .= "Dropping conflicting bug alias ";
614 $err .= $bug_fields{'alias'} . "\n";
617 $alias = $bug_fields{'alias'};
618 push @query, 'alias';
619 push @values, $alias;
624 push( @query, "creation_ts" );
626 format_time
( $bug_fields{'creation_ts'}, "%Y-%m-%d %X" )
629 push( @query, "delta_ts" );
631 format_time
( $bug_fields{'delta_ts'}, "%Y-%m-%d %X" )
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
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"}
654 if ( defined $bug_fields{'product'} ) {
655 $product = new Bugzilla
::Product
( { name
=> $bug_fields{'product'} } );
657 $product = $def_product;
658 $err .= "Unknown Product " . $bug_fields{'product'} . "\n";
659 $err .= " Using default product set in Parameters \n";
663 $product = $def_product;
665 if ( defined $bug_fields{'component'} ) {
666 $component = new Bugzilla
::Component
(
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";
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
696 my $version = new Bugzilla
::Version
(
697 { product
=> $product, name
=> $bug_fields{'version'} });
699 push( @query, "version" );
701 push( @values, $version->name );
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'}
711 $err .= " in product " . $product->name . ". \n";
712 $err .= " Setting version to \"" . $v->name . "\".\n";
716 if ( $params->{"usetargetmilestone"} ) {
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'} });
725 push( @values, $milestone->name );
728 push( @values, $product->default_milestone );
729 $err .= "Unknown milestone \"";
730 $err .= ( defined $bug_fields{'target_milestone'} )
731 ?
$bug_fields{'target_milestone'}
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'},
746 push( @values, $bug_fields{'bug_severity'} );
749 push( @values, $params->{'defaultseverity'} );
750 $err .= "Unknown severity ";
751 $err .= ( defined $bug_fields{'bug_severity'} )
752 ?
$bug_fields{'bug_severity'}
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'},
763 push( @values, $bug_fields{'priority'} );
766 push( @values, $params->{'defaultpriority'} );
767 $err .= "Unknown priority ";
768 $err .= ( defined $bug_fields{'priority'} )
769 ?
$bug_fields{'priority'}
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'},
780 push( @values, $bug_fields{'rep_platform'} );
783 push( @values, $params->{'defaultplatform'} );
784 $err .= "Unknown platform ";
785 $err .= ( defined $bug_fields{'rep_platform'} )
786 ?
$bug_fields{'rep_platform'}
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'},
797 push( @values, $bug_fields{'op_sys'} );
800 push( @values, $params->{'defaultopsys'} );
801 $err .= "Unknown operating system ";
802 $err .= ( defined $bug_fields{'op_sys'} )
803 ?
$bug_fields{'op_sys'}
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" )
813 push( @values, $date );
814 push( @query, "deadline" );
815 if ( defined $bug_fields{'estimated_time'} ) {
817 Bugzilla
::Bug
::ValidateTime
($bug_fields{'estimated_time'}, "e");
820 push( @values, $bug_fields{'estimated_time'} );
821 push( @query, "estimated_time" );
824 if ( defined $bug_fields{'remaining_time'} ) {
826 Bugzilla
::Bug
::ValidateTime
($bug_fields{'remaining_time'}, "r");
829 push( @values, $bug_fields{'remaining_time'} );
830 push( @query, "remaining_time" );
833 if ( defined $bug_fields{'actual_time'} ) {
835 Bugzilla
::Bug
::ValidateTime
($bug_fields{'actual_time'}, "a");
838 $bug_fields{'actual_time'} = 0.0;
839 $err .= "Invalid Actual Time. Setting to 0.0\n";
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 );
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";
865 $err .= " Previous reporter is unknown.\n";
869 my $changed_owner = 0;
871 push( @query, "assigned_to" );
872 if ( ( $bug_fields{'assigned_to'} )
873 && ( $owner = login_to_id
( $bug_fields{'assigned_to'} )) ) {
874 push( @values, $owner );
877 push( @values, $component->default_assignee->id );
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";
886 $err .= " Previous assignee is unknown.\n";
890 if ( $params->{"useqacontact"} ) {
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 );
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'},
912 my $valid_status = check_field
('bug_status',
913 scalar $bug_fields{'bug_status'},
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
921 if ($product->votes_to_confirm) {
922 $everconfirmed = $bug_fields{'everconfirmed'} || 0;
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;
946 if (scalar(@bug_statuses)) {
947 $initial_status = $bug_statuses[0]->name;
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;
958 # There is NO other open bug statuses outside UNCO???
959 Error
("no open bug statuses available.");
967 $err .= "Resolution set on an open status.\n";
968 $err .= " Dropping resolution $resolution\n";
973 $status = $initial_status;
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";
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
1002 $err .= "Missing Resolution. Setting status to ";
1004 $status = $initial_status;
1005 $err .= "$initial_status\n";
1008 $status = "UNCONFIRMED";
1009 $err .= "UNCONFIRMED\n";
1013 $err .= "Unknown resolution \"$resolution\".\n";
1014 $err .= " Setting resolution to MOVED\n";
1015 $resolution = "MOVED";
1019 else{ # $valid_status is false
1021 $status = $initial_status;
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
1035 $status = $initial_status;
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 );
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);
1072 $err .= "Skipping illegal value \"$value\" in $custom_field.\n" ;
1074 } elsif ($field->type == FIELD_TYPE_MULTI_SELECT
) {
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);
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); };
1090 $err .= "Skipping illegal value \"$value\" in $custom_field.\n" ;
1093 push(@query, $custom_field);
1094 push(@values, $value);
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
1105 push @query, "lastdiffed";
1106 push @values, $timestamp;
1110 my $query = "INSERT INTO bugs (" . join( ", ", @query ) . ") VALUES (";
1111 $query .= '?,' foreach (@values);
1112 chop($query); # Remove the last comma.
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
1121 no warnings
'uninitialized';
1123 "Bug Query: INSERT INTO bugs (\n"
1124 . join( ",\n", @query )
1126 . join( ",\n", @values ),
1132 if ( defined $bug_fields{'cc'} ) {
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;
1138 if ($uid = login_to_id
($person)) {
1139 if ( !$ccseen{$uid} ) {
1140 $sth_cc->execute( $id, $uid );
1145 $err .= "CC member $person does not have an account here\n";
1151 if ( defined( $bug_fields{'keywords'} ) ) {
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";
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
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
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);
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'},
1202 # Insert Attachments for the bug
1203 foreach my $att (@attachments) {
1205 $err .= "No attachment ID specified, dropping attachment\n";
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 );
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";
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'},
1256 # Clear the attachments array for the next bug
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;
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)
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";
1288 $log .= "The following problems were encountered while creating bug $id.\n";
1290 $log .= "You may have to set certain fields in the new bug by hand.\n\n";
1292 Debug
( $log, OK_LEVEL
);
1294 Bugzilla
::BugMail
::Send
( $id, { 'changer' => $exporter_login } ) if ($mail);
1296 # done with the xml data. Lets clear it from memory
1301 Debug
( "Reading xml", DEBUG_LEVEL
);
1303 # Read STDIN in slurp mode. VERY dangerous, but we live on the wild side ;-)
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(
1328 bug
=> \
&process_bug
,
1329 attachment
=> \
&process_attachment
1331 start_tag_handlers
=> { bugzilla
=> \
&init
}
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 );
1351 importxml - Import bugzilla bug data from xml.
1355 importxml.pl [options] [file ...]
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.)
1371 Print a brief help message and exits.
1375 Print error and debug information. Mulltiple -v increases verbosity
1379 Send mail to exporter with a log of bugs imported and any errors.
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