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