#!/usr/bin/perl # $Id: service.in,v 1.118 2005/10/19 01:22:14 don Exp $ # # Usage: service .nn # Temps: incoming/P.nn use warnings; use strict; use Debbugs::Config qw(:globals :config); use File::Copy; use MIME::Parser; use Params::Validate qw(:types validate_with); use Debbugs::Common qw(:util :quit :misc :lock); use Debbugs::Status qw(:read :status :write :versions :hook); use Debbugs::Packages qw(binary_to_source); use Debbugs::MIME qw(decode_rfc1522 encode_rfc1522 create_mime_message); use Debbugs::Mail qw(send_mail_message); use Debbugs::User; use Debbugs::Recipients qw(:all); use HTML::Entities qw(encode_entities); use Debbugs::Versions::Dpkg; use Debbugs::Status qw(splitpackages); use Debbugs::CGI qw(html_escape); use Debbugs::Control qw(:all); use Debbugs::Log qw(:misc); use Debbugs::Text qw(:templates); use Scalar::Util qw(looks_like_number); use List::Util qw(first); use Mail::RFC822::Address; use Encode qw(decode encode); chdir($config{spool_dir}) or die "Unable to chdir to spool_dir '$config{spool_dir}': $!"; my $debug = 0; umask(002); my ($nn,$control) = $ARGV[0] =~ m/^(([RC])\.\d+)$/; if (not defined $control or not defined $nn) { die "Bad argument to service.in"; } if (!rename("incoming/G$nn","incoming/P$nn")) { defined $! and $! =~ m/no such file or directory/i and exit 0; die "Failed to rename incoming/G$nn to incoming/P$nn: $!"; } my $log_fh = IO::File->new("incoming/P$nn",'r') or die "Unable to open incoming/P$nn for reading: $!"; my @log=<$log_fh>; my @msg=@log; close($log_fh); chomp @msg; print "###\n",join("##\n",@msg),"\n###\n" if $debug; # Bug numbers to send e-mail to, hash so that we don't send to the # same bug twice. my (%bug_affected); my (@headerlines,@bodylines); my $parse_output = Debbugs::MIME::parse(join('',@log)); @headerlines = @{$parse_output->{header}}; @bodylines = @{$parse_output->{body}}; my %header; for (@headerlines) { $_ = decode_rfc1522($_); s/\n\s/ /g; print ">$_<\n" if $debug; if (s/^(\S+):\s*//) { my $v = lc $1; print ">$v=$_<\n" if $debug; $header{$v} = $_; } else { print "!>$_<\n" if $debug; } } $header{'message-id'} ||= ''; $header{subject} ||= ''; grep(s/\s+$//,@bodylines); print "***\n",join("\n",@bodylines),"\n***\n" if $debug; if (defined $header{'resent-from'} && !defined $header{'from'}) { $header{'from'} = $header{'resent-from'}; } defined($header{'from'}) || die "no From header"; delete $header{'reply-to'} if ( defined($header{'reply-to'}) && $header{'reply-to'} =~ m/^\s*$/ ); my $replyto; if ( defined($header{'reply-to'}) && $header{'reply-to'} ne "" ) { $replyto = $header{'reply-to'}; } else { $replyto = $header{'from'}; } # This is an error counter which should be incremented every time there is an error. my $errors = 0; my $controlrequestaddr= ($control ? 'control' : 'request').'@'.$config{email_domain}; my $transcript_scalar = ''; open my $transcript, ">:scalar:utf8", \$transcript_scalar or die "Unable to create transcript scalar: $!"; print {$transcript} "Processing commands for $controlrequestaddr:\n\n"; my $dl = 0; my %affected_packages; my %recipients; # this is the hashref which is passed to all control calls my %limit = (); my @common_control_options = (transcript => $transcript, requester => $header{from}, request_addr => $controlrequestaddr, request_msgid => $header{'message-id'}, request_subject => $header{subject}, request_nn => $nn, request_replyto => $replyto, message => \@log, affected_bugs => \%bug_affected, affected_packages => \%affected_packages, recipients => \%recipients, limit => \%limit, ); my $state= 'idle'; my $lowstate= 'idle'; my $mergelowstate= 'idle'; my $midix=0; my $user = $replyto; $user =~ s/,.*//; $user =~ s/^.*<(.*)>.*$/$1/; $user =~ s/[(].*[)]//; $user =~ s/^\s*(\S+)\s+.*$/$1/; $user = "" unless (Debbugs::User::is_valid_user($user)); my $indicated_user = 0; my $quickabort = 0; if (@gExcludeFromControl and grep {$replyto =~ m/\Q$_\E/} @gExcludeFromControl) { print {$transcript} fill_template('mail/excluded_from_control'); $quickabort = 1; } my %limit_pkgs = (); my %clonebugs = (); my %bcc = (); my @bcc; sub addbcc { push @bcc, $_[0] unless grep { $_ eq $_[0] } @bcc; } our $data; our $message; our $extramessage; our $ref; our $mismatch; our $action; my $ok = 0; my $unknowns = 0; my $procline=0; for ($procline=0; $procline<=$#bodylines; $procline++) { my $noriginator; my $newsubmitter; my $oldsubmitter; my $newowner; $state eq 'idle' || print "state: $state ?\n"; $lowstate eq 'idle' || print "lowstate: $lowstate ?\n"; $mergelowstate eq 'idle' || print "mergelowstate: $mergelowstate ?\n"; if ($quickabort) { print {$transcript} "Stopping processing here.\n\n"; last; } $_= $bodylines[$procline]; s/\s+$//; # Remove BOM markers from UTF-8 strings # Fixes #488554 s/\xef\xbb\xbf//g; next unless m/\S/; eval { my $temp = decode("utf8",$_,Encode::FB_CROAK); $_ = $temp; }; print {$transcript} "> $_\n"; next if m/^\s*\#/; $action= ''; if (m/^(?:stop|quit|--|thank(?:s|\s*you)?|kthxbye)\.*\s*$/i) { print {$transcript} "Stopping processing here.\n\n"; last; } elsif (m/^debug\s+(\d+)$/i && $1 >= 0 && $1 <= 1000) { $dl= $1+0; if ($dl > 0 and not grep /debug/,@common_control_options) { push @common_control_options,(debug => $transcript); } print {$transcript} "Debug level $dl.\n\n"; } elsif (m/^(send|get)\s+\#?(\d{2,})$/i) { $ref= $2+0; &sendlynxdoc("bugreport.cgi?bug=$ref","logs for $gBug#$ref"); } elsif (m/^send-detail\s+\#?(\d{2,})$/i) { $ref= $1+0; &sendlynxdoc("bugreport.cgi?bug=$ref&boring=yes", "detailed logs for $gBug#$ref"); } elsif (m/^index(\s+full)?$/i) { print {$transcript} "This BTS function is currently disabled, sorry.\n\n"; $errors++; $ok++; # well, it's not really ok, but it fixes #81224 :) } elsif (m/^index-summary\s+by-package$/i) { print {$transcript} "This BTS function is currently disabled, sorry.\n\n"; $errors++; $ok++; # well, it's not really ok, but it fixes #81224 :) } elsif (m/^index-summary(\s+by-number)?$/i) { print {$transcript} "This BTS function is currently disabled, sorry.\n\n"; $errors++; $ok++; # well, it's not really ok, but it fixes #81224 :) } elsif (m/^index(\s+|-)pack(age)?s?$/i) { &sendlynxdoc("pkgindex.cgi?indexon=pkg",'index of packages'); } elsif (m/^index(\s+|-)maints?$/i) { &sendlynxdoc("pkgindex.cgi?indexon=maint",'index of maintainers'); } elsif (m/^index(\s+|-)maint\s+(\S+)$/i) { my $maint = $2; &sendlynxdoc("pkgreport.cgi?maint=" . urlsanit($maint), "$gBug list for maintainer \`$maint'"); $ok++; } elsif (m/^index(\s+|-)pack(age)?s?\s+(\S.*\S)$/i) { my $package = $+; &sendlynxdoc("pkgreport.cgi?pkg=" . urlsanit($package), "$gBug list for package $package"); $ok++; } elsif (m/^send-unmatched(\s+this|\s+-?0)?$/i) { print {$transcript} "This BTS function is currently disabled, sorry.\n\n"; $errors++; $ok++; # well, it's not really ok, but it fixes #81224 :) } elsif (m/^send-unmatched\s+(last|-1)$/i) { print {$transcript} "This BTS function is currently disabled, sorry.\n\n"; $errors++; $ok++; # well, it's not really ok, but it fixes #81224 :) } elsif (m/^send-unmatched\s+(old|-2)$/i) { print {$transcript} "This BTS function is currently disabled, sorry.\n\n"; $errors++; $ok++; # well, it's not really ok, but it fixes #81224 :) } elsif (m/^getinfo\s+([\w.-]+)$/i) { # the following is basically a Debian-specific kludge, but who cares my $req = $1; if ($req =~ /^maintainers$/i && -f "$gConfigDir/Maintainers") { &sendinfo("local", "$gConfigDir/Maintainers", "Maintainers file"); } elsif ($req =~ /^override\.(\w+)\.([\w.-]+)$/i) { $req =~ s/.gz$//; &sendinfo("ftp.d.o", "$req", "override file for $2 part of $1 distribution"); } elsif ($req =~ /^pseudo-packages\.(description|maintainers)$/i && -f "$gConfigDir/$req") { &sendinfo("local", "$gConfigDir/$req", "$req file"); } else { print {$transcript} "Info file $req does not exist.\n\n"; } } elsif (m/^help/i) { &sendhelp; print {$transcript} "\n"; $ok++; } elsif (m/^refcard/i) { &sendtxthelp("bug-mailserver-refcard.txt","mail servers' reference card"); } elsif (m/^subscribe/i) { print {$transcript} < $bodylines[$procline]\n"; next if $bad; my ($o, $txt) = ($1, $2); if ($#cats == -1 && $o eq "+") { print {$transcript} "User defined category specification must start with a category name. Skipping.\n\n"; $errors++; $bad = 1; next; } if ($o eq "+") { unless (ref($cats[-1]) eq "HASH") { $cats[-1] = { "nam" => $cats[-1], "pri" => [], "ttl" => [] }; } $catsec++; my ($desc, $ord, $op); if ($txt =~ m/^(.*\S)\s*\[((\d+):\s*)?\]\s*$/) { $desc = $1; $ord = $3; $op = ""; } elsif ($txt =~ m/^(.*\S)\s*\[((\d+):\s*)?(\S+)\]\s*$/) { $desc = $1; $ord = $3; $op = $4; } elsif ($txt =~ m/^([^[\s]+)\s*$/) { $desc = ""; $op = $1; } else { print {$transcript} "Unrecognised syntax for category section. Skipping.\n\n"; $errors++; $bad = 1; next; } $ord = 999 unless defined $ord; if ($op) { push @{$cats[-1]->{"pri"}}, $prefix . $op; push @{$cats[-1]->{"ttl"}}, $desc; push @ords, "$ord $catsec"; } else { $cats[-1]->{"def"} = $desc; push @ords, "$ord DEF"; $catsec--; } @ords = sort { my ($a1, $a2, $b1, $b2) = split / /, "$a $b"; ((looks_like_number($a1) and looks_like_number($a2))?$a1 <=> $b1:$a1 cmp $b1) || ((looks_like_number($a2) and looks_like_number($b2))?$a2 <=> $b2:$a2 cmp $b2); } @ords; $cats[-1]->{"ord"} = [map { m/^.* (\S+)/; $1 eq "DEF" ? $catsec + 1 : $1 } @ords]; } elsif ($o eq "*") { $catsec = 0; my ($name); if ($txt =~ m/^(.*\S)(\s*\[(\S+)\])\s*$/) { $name = $1; $prefix = $3; } else { $name = $txt; $prefix = ""; } push @cats, $name; } } # XXX: got @cats, now do something with it my $u = Debbugs::User::get_user($user); if (@cats) { print {$transcript} "Added usercategory $catname.\n\n"; $u->{"categories"}->{$catname} = [ @cats ]; if (not $hidden) { push @{$u->{visible_cats}},$catname; } } else { print {$transcript} "Removed usercategory $catname.\n\n"; delete $u->{"categories"}->{$catname}; @{$u->{visible_cats}} = grep {$_ ne $catname} @{$u->{visible_cats}}; } $u->write(); } elsif (m/^usertags?\s+\#?(-?\d+)\s+(([=+-])\s*)?(\S.*)?$/i) { $ok++; $ref = $1; my $addsubcode = $3 || "+"; my $tags = $4; if ($ref =~ m/^-\d+$/ && defined $clonebugs{$ref}) { $ref = $clonebugs{$ref}; } if ($user eq "") { print {$transcript} "No valid user selected\n"; $errors++; $indicated_user = 1; } elsif (&setbug) { if (not $indicated_user and defined $user) { print {$transcript} "User is $user\n"; $indicated_user = 1; } &nochangebug; my %ut; Debbugs::User::read_usertags(\%ut, $user); my @oldtags = (); my @newtags = (); my @badtags = (); my %chtags; if (defined $tags and length $tags) { for my $t (split /[,\s]+/, $tags) { if ($t =~ m/^[a-zA-Z0-9.+\@-]+$/) { $chtags{$t} = 1; } else { push @badtags, $t; } } } if (@badtags) { print {$transcript} "Ignoring illegal tag/s: ".join(', ', @badtags).".\nPlease use only alphanumerics, at, dot, plus and dash.\n"; $errors++; } for my $t (keys %chtags) { $ut{$t} = [] unless defined $ut{$t}; } for my $t (keys %ut) { my %res = map { ($_, 1) } @{$ut{$t}}; push @oldtags, $t if defined $res{$ref}; my $addop = ($addsubcode eq "+" or $addsubcode eq "="); my $del = (defined $chtags{$t} ? $addsubcode eq "-" : $addsubcode eq "="); $res{$ref} = 1 if ($addop && defined $chtags{$t}); delete $res{$ref} if ($del); push @newtags, $t if defined $res{$ref}; $ut{$t} = [ sort { $a <=> $b } (keys %res) ]; } if (@oldtags == 0) { print {$transcript} "There were no usertags set.\n"; } else { print {$transcript} "Usertags were: " . join(" ", @oldtags) . ".\n"; } print {$transcript} "Usertags are now: " . join(" ", @newtags) . ".\n"; Debbugs::User::write_usertags(\%ut, $user); } } elsif (!$control) { print {$transcript} <= 3) { print {$transcript} "Too many unknown commands, stopping here.\n\n"; last; } #### "developer only" ones start here } elsif (m/^close\s+\#?(-?\d+)(?:\s+(\d.*))?$/i) { $ok++; $ref= $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; if (defined $2) { eval { set_fixed(@common_control_options, bug => $ref, fixed => $2, add => 1, ); }; if ($@) { $errors++; print {$transcript} "Failed to add fixed version '$2' to $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } eval { set_done(@common_control_options, done => 1, bug => $ref, reopen => 0, notify_submitter => 1, clear_fixed => 0, ); }; if ($@) { $errors++; print {$transcript} "Failed to mark $ref as done: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^reassign\s+\#?(-?\d+)\s+ # bug and command (?:(?:((?:src:|source:)?$config{package_name_re}) # new package (?:\s+((?:$config{package_name_re}\/)? $config{package_version_re}))?)| # optional version ((?:src:|source:)?$config{package_name_re} # multiple package form (?:\s*\,\s*(?:src:|source:)?$config{package_name_re})+)) \s*$/xi) { $ok++; $ref= $1; my @new_packages; if (not defined $2) { push @new_packages, split /\s*\,\s*/,$4; } else { push @new_packages, $2; } @new_packages = map {y/A-Z/a-z/; s/^(?:src|source):/src:/; $_;} @new_packages; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; my $version= $3; eval { set_package(@common_control_options, bug => $ref, package => \@new_packages, ); # if there is a version passed, we make an internal call # to set_found if (defined($version) && length $version) { set_found(@common_control_options, bug => $ref, found => $version, ); } }; if ($@) { $errors++; print {$transcript} "Failed to clear fixed versions and reopen on $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^reopen\s+\#?(-?\d+)(?:\s+([\=\!]|(?:\S.*\S)))?$/i) { $ok++; $ref= $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; my $new_submitter = $2; if (defined $new_submitter) { if ($new_submitter eq '=') { undef $new_submitter; } elsif ($new_submitter eq '!') { $new_submitter = $replyto; } } eval { set_done(@common_control_options, bug => $ref, reopen => 1, defined $new_submitter? (submitter => $new_submitter):(), ); }; if ($@) { $errors++; print {$transcript} "Failed to reopen $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m{^(?:(?i)found)\s+\#?(-?\d+) (?:\s+((?:$config{package_name_re}\/)? $config{package_version_re} # allow for multiple packages (?:\s*,\s*(?:$config{package_name_re}\/)? $config{package_version_re})*) )?$}x) { $ok++; $ref= $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; my @versions; if (defined $2) { @versions = split /\s*,\s*/,$2; eval { set_found(@common_control_options, bug => $ref, found => \@versions, add => 1, ); }; if ($@) { $errors++; print {$transcript} "Failed to add found on $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } else { eval { set_fixed(@common_control_options, bug => $ref, fixed => [], reopen => 1, ); }; if ($@) { $errors++; print {$transcript} "Failed to clear fixed versions and reopen on $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } } elsif (m{^(?:(?i)notfound)\s+\#?(-?\d+) \s+((?:$config{package_name_re}\/)? $config{package_version_re} # allow for multiple packages (?:\s*,\s*(?:$config{package_name_re}\/)? $config{package_version_re})* )$}x) { $ok++; $ref= $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; my @versions; @versions = split /\s*,\s*/,$2; eval { set_found(@common_control_options, bug => $ref, found => \@versions, remove => 1, ); }; if ($@) { $errors++; print {$transcript} "Failed to remove found on $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m{^(?:(?i)fixed)\s+\#?(-?\d+) \s+((?:$config{package_name_re}\/)? $config{package_version_re} # allow for multiple packages (?:\s*,\s*(?:$config{package_name_re}\/)? $config{package_version_re})*) \s*$}x) { $ok++; $ref= $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; my @versions; @versions = split /\s*,\s*/,$2; eval { set_fixed(@common_control_options, bug => $ref, fixed => \@versions, add => 1, ); }; if ($@) { $errors++; print {$transcript} "Failed to add fixed on $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m{^(?:(?i)notfixed)\s+\#?(-?\d+) \s+((?:$config{package_name_re}\/)? $config{package_version_re} # allow for multiple packages (?:\s*,\s*(?:$config{package_name_re}\/)? $config{package_version_re})*) \s*$}x) { $ok++; $ref= $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; my @versions; @versions = split /\s*,\s*/,$2; eval { set_fixed(@common_control_options, bug => $ref, fixed => \@versions, remove => 1, ); }; if ($@) { $errors++; print {$transcript} "Failed to remove fixed on $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^submitter\s+\#?(-?\d+)\s+(\!|\S.*\S)$/i) { $ok++; $ref= $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; my $newsubmitter = $2 eq '!' ? $replyto : $2; if (not Mail::RFC822::Address::valid($newsubmitter)) { print {$transcript} "$newsubmitter is not a valid e-mail address; not changing submitter\n"; $errors++; } else { eval { set_submitter(@common_control_options, bug => $ref, submitter => $newsubmitter, ); }; if ($@) { $errors++; print {$transcript} "Failed to set submitter on $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } } elsif (m/^forwarded\s+\#?(-?\d+)\s+(\S.*\S)$/i) { $ok++; $ref= $1; my $forward_to= $2; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; eval { set_forwarded(@common_control_options, bug => $ref, forwarded => $forward_to, ); }; if ($@) { $errors++; print {$transcript} "Failed to set the forwarded-to-address of $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^notforwarded\s+\#?(-?\d+)$/i) { $ok++; $ref= $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; eval { set_forwarded(@common_control_options, bug => $ref, forwarded => undef, ); }; if ($@) { $errors++; print {$transcript} "Failed to clear the forwarded-to-address of $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^(?:severity|priority)\s+\#?(-?\d+)\s+([-0-9a-z]+)$/i) { $ok++; $ref= $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; my $newseverity= $2; if (exists $gObsoleteSeverities{$newseverity}) { print {$transcript} "Severity level \`$newseverity' is obsolete. " . "Use $gObsoleteSeverities{$newseverity} instead.\n\n"; $errors++; } elsif (not defined first {$_ eq $newseverity} (@gSeverityList, "$gDefaultSeverity")) { print {$transcript} "Severity level \`$newseverity' is not known.\n". "Recognized are: $gShowSeverities.\n\n"; $errors++; } else { eval { set_severity(@common_control_options, bug => $ref, severity => $newseverity, ); }; if ($@) { $errors++; print {$transcript} "Failed to set severity of $config{bug} $ref to $newseverity: ".cleanup_eval_fail($@,$debug)."\n"; } } } elsif (m/^tags?\s+\#?(-?\d+)\s+(\S.*)$/i) { $ok++; $ref = $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; my $tags = $2; my @tags = map {m/^([+=-])(.+)/ ? ($1,$2):($_)} split /[\s,]+/, $tags; # this is an array of hashrefs which contain two elements, the # first of which is the array of tags, the second is the # option to pass to set_tag (we use a hashref here to make it # more obvious what is happening) my @tag_operations; my @badtags; for my $tag (@tags) { if ($tag =~ /^[=+-]$/) { if ($tag eq '=') { @tag_operations = {tags => [], option => [], }; } elsif ($tag eq '-') { push @tag_operations, {tags => [], option => [remove => 1], }; } elsif ($tag eq '+') { push @tag_operations, {tags => [], option => [add => 1], }; } next; } if (not defined first {$_ eq $tag} @{$config{tags}}) { push @badtags, $tag; next; } if (not @tag_operations) { @tag_operations = {tags => [], option => [add => 1], }; } push @{$tag_operations[-1]{tags}},$tag; } if (@badtags) { print {$transcript} "Unknown tag/s: ".join(', ', @badtags).".\n". "Recognized are: ".join(' ', @gTags).".\n\n"; $errors++; } eval { for my $operation (@tag_operations) { set_tag(@common_control_options, bug => $ref, tag => [@{$operation->{tags}}], warn_on_bad_tags => 0, # don't warn on bad tags, # 'cause we do that above @{$operation->{option}}, ); } }; if ($@) { # we intentionally have two errors here if there is a bad # tag and the above fails for some reason $errors++; print {$transcript} "Failed to alter tags of $config{bug} $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^(un)?block\s+\#?(-?\d+)\s+(?:by|with)\s+(\S.*)?$/i) { $ok++; $ref= $2; my $add_remove = defined $1 && $1 eq 'un'; my @blockers = map {exists $clonebugs{$_}?$clonebugs{$_}:$_} split /[\s,]+/, $3; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; eval { set_blocks(@common_control_options, bug => $ref, block => \@blockers, $add_remove ? (remove => 1):(add => 1), ); }; if ($@) { $errors++; print {$transcript} "Failed to set blocking bugs of $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^retitle\s+\#?(-?\d+)\s+(\S.*\S)\s*$/i) { $ok++; $ref= $1; my $newtitle= $2; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; eval { set_title(@common_control_options, bug => $ref, title => $newtitle, ); }; if ($@) { $errors++; print {$transcript} "Failed to set the title of $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^unmerge\s+\#?(-?\d+)$/i) { $ok++; $ref= $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; eval { set_merged(@common_control_options, bug => $ref, ); }; if ($@) { $errors++; print {$transcript} "Failed to unmerge $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^merge\s+#?(-?\d+(\s+#?-?\d+)+)\s*$/i) { $ok++; my @tomerge; ($ref,@tomerge) = map {exists $clonebugs{$_}?$clonebugs{$_}:$_} split(/\s+#?/,$1); eval { set_merged(@common_control_options, bug => $ref, merge_with => \@tomerge, ); }; if ($@) { $errors++; print {$transcript} "Failed to merge $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^forcemerge\s+\#?(-?\d+(?:\s+\#?-?\d+)+)\s*$/i) { $ok++; my @tomerge; ($ref,@tomerge) = map {exists $clonebugs{$_}?$clonebugs{$_}:$_} split(/\s+#?/,$1); eval { set_merged(@common_control_options, bug => $ref, merge_with => \@tomerge, force => 1, masterbug => 1, ); }; if ($@) { $errors++; print {$transcript} "Failed to forcibly merge $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^clone\s+#?(\d+)\s+((-\d+\s+)*-\d+)\s*$/i) { $ok++; my $origref = $1; my @newclonedids = split /\s+/, $2; my $newbugsneeded = scalar(@newclonedids); $ref = $origref; if (exists $clonebugs{$ref}) { $ref = $clonebugs{$ref}; } $bug_affected{$ref} = 1; eval { my %new_clones; clone_bug(@common_control_options, bug => $ref, new_bugs => \@newclonedids, new_clones => \%new_clones, ); %clonebugs = (%clonebugs, %new_clones); }; if ($@) { $errors++; print {$transcript} "Failed to clone $ref: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^package\:?\s+(\S.*\S)?\s*$/i) { $ok++; my @pkgs = split /\s+/, $1; if (scalar(@pkgs) > 0) { %limit_pkgs = map { ($_, 1) } @pkgs; $limit{package} = [@pkgs]; print {$transcript} "Limiting to bugs with field 'package' containing at least one of ".join(', ',map {qq('$_')} @pkgs)."\n"; print {$transcript} "Limit currently set to"; for my $limit_field (keys %limit) { print {$transcript} " '$limit_field':".join(', ',map {qq('$_')} @{$limit{$limit_field}})."\n"; } print {$transcript} "\n"; } else { %limit_pkgs = (); $limit{package} = []; print {$transcript} "Limit cleared.\n\n"; } } elsif (m/^limit\:?\s+(\S.*\S)\s*$/) { $ok++; my ($field,@options) = split /\s+/, $1; $field = lc($field); if ($field =~ /^(?:clear|unset|blank)$/) { %limit = (); print {$transcript} "Limit cleared.\n\n"; } elsif (exists $Debbugs::Status::fields{$field} or $field eq 'source') { # %limit can actually contain regexes, but because they're # not evaluated in Safe, DO NOT allow them through without # fixing this. $limit{$field} = [@options]; print {$transcript} "Limiting to bugs with field '$field' containing at least one of ".join(', ',map {qq('$_')} @options)."\n"; print {$transcript} "Limit currently set to"; for my $limit_field (keys %limit) { print {$transcript} " '$limit_field':".join(', ',map {qq('$_')} @{$limit{$limit_field}})."\n"; } print {$transcript} "\n"; } else { print {$transcript} "Limit key $field not understood. Stopping processing here.\n\n"; $errors++; last; } } elsif (m/^affects?\s+\#?(-?\d+)(?:\s+((?:[=+-])?)\s*(\S.*)?)?\s*$/i) { $ok++; $ref = $1; my $add_remove = $2 || ''; my $packages = $3 || ''; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; eval { affects(@common_control_options, bug => $ref, package => [splitpackages($3)], ($add_remove eq '+'?(add => 1):()), ($add_remove eq '-'?(remove => 1):()), ); }; if ($@) { $errors++; print {$transcript} "Failed to mark $ref as affecting package(s): ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^summary\s+\#?(-?\d+)\s*(\d+|)\s*$/i) { $ok++; $ref = $1; my $summary_msg = length($2)?$2:undef; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; eval { summary(@common_control_options, bug => $ref, summary => $summary_msg, ); }; if ($@) { $errors++; print {$transcript} "Failed to give $ref a summary: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^owner\s+\#?(-?\d+)\s+((?:\S.*\S)|\!)\s*$/i) { $ok++; $ref = $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; my $newowner = $2; if ($newowner eq '!') { $newowner = $replyto; } eval { owner(@common_control_options, bug => $ref, owner => $newowner, ); }; if ($@) { $errors++; print {$transcript} "Failed to mark $ref as having an owner: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^noowner\s+\#?(-?\d+)\s*$/i) { $ok++; $ref = $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; eval { owner(@common_control_options, bug => $ref, owner => undef, ); }; if ($@) { $errors++; print {$transcript} "Failed to mark $ref as not having an owner: ".cleanup_eval_fail($@,$debug)."\n"; } } elsif (m/^unarchive\s+#?(\d+)$/i) { $ok++; $ref = $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; eval { bug_unarchive(@common_control_options, bug => $ref, recipients => \%recipients, ); }; if ($@) { $errors++; } } elsif (m/^archive\s+#?(\d+)$/i) { $ok++; $ref = $1; $ref = $clonebugs{$ref} if exists $clonebugs{$ref}; eval { bug_archive(@common_control_options, bug => $ref, ignore_time => 1, archive_unarchived => 0, ); }; if ($@) { $errors++; } } else { print {$transcript} "Unknown command or malformed arguments to command.\n\n"; $errors++; if (++$unknowns >= 5) { print {$transcript} "Too many unknown commands, stopping here.\n\n"; last; } } } if ($procline>$#bodylines) { print {$transcript} ">\nEnd of message, stopping processing here.\n\n"; } if (!$ok && !$quickabort) { $errors++; print {$transcript} "No commands successfully parsed; sending the help text(s).\n"; &sendhelp; print {$transcript} "\n"; } my @maintccs = determine_recipients(recipients => \%recipients, address_only => 1, cc => 1, ); my $maintccs = 'Cc: '.join(",\n ", determine_recipients(recipients => \%recipients, cc => 1, ) )."\n"; my $packagepr = ''; $packagepr = "X-${gProject}-PR-Package: " . join(keys %affected_packages) . "\n" if keys %affected_packages; # Add Bcc's to subscribed bugs # now handled by Debbugs::Recipients #push @bcc, map {"bugs=$_\@$gListDomain"} keys %bug_affected; if (!defined $header{'subject'} || $header{'subject'} eq "") { $header{'subject'} = "your mail"; } # Error text here advertises how many errors there were my $error_text = $errors > 0 ? " (with $errors errors)":''; my @common_headers; push @common_headers, 'X-Loop',$gMaintainerEmail; my $temp_transcript = ${transcript_scalar}; eval{ $temp_transcript = decode("utf8",$temp_transcript,Encode::FB_CROAK); }; my $reply = create_mime_message([From => "$gMaintainerEmail ($gProject $gBug Tracking System)", To => $replyto, @maintccs ? (Cc => join(', ',@maintccs)):(), Subject => "Processed${error_text}: $header{subject}", 'Message-ID' => "", 'In-Reply-To' => $header{'message-id'}, References => join(' ',grep {defined $_} $header{'message-id'},$data->{msgid}), Precedence => 'bulk', keys %affected_packages ?("X-${gProject}-PR-Package" => join(' ',keys %affected_packages)):(), keys %affected_packages ?("X-${gProject}-PR-Source" => join(' ', map {defined $_ ?(ref($_)?@{$_}:$_):()} binary_to_source(binary => [keys %affected_packages], source_only => 1))):(), "X-$gProject-PR-Message" => 'transcript', @common_headers, ], fill_template('mail/message_body', {body => "${temp_transcript}Please contact me if you need assistance."}, )); my $repliedshow= join(', ',$replyto, determine_recipients(recipients => \%recipients, cc => 1, address_only => 1, ) ); # -1 is the service.in log &filelock("lock/-1"); open(AP,">>db-h/-1.log") || die "open db-h/-1.log: $!"; print(AP "\2\n$repliedshow\n\5\n$reply\n\3\n". "\6\n". "Request received from ". html_escape($header{'from'})."\n". "to ".html_escape($controlrequestaddr)."\n". "\3\n". "\7\n",escape_log(@log),"\n\3\n") || die "writing db-h/-1.log: $!"; close(AP) || die "open db-h/-1.log: $!"; &unfilelock; utime(time,time,"db-h"); &sendmailmessage($reply, exists $header{'x-debbugs-no-ack'}?():$replyto, make_list(values %{{determine_recipients(recipients => \%recipients, address_only => 1, )}} ), ); unlink("incoming/P$nn") || die "unlinking incoming/P$nn: $!"; sub sendmailmessage { my ($message,@recips) = @_; $message = "X-Loop: $gMaintainerEmail\n" . $message; send_mail_message(message => $message, recipients => \@recips, ); $midix++; } sub fill_template{ my ($template,$extra_var) = @_; $extra_var ||={}; my $variables = {config => \%config, defined($ref)?(ref => $ref):(), defined($data)?(data => $data):(), refs => [map {exists $clonebugs{$_}?$clonebugs{$_}:$_} keys %bug_affected], %{$extra_var}, }; my $hole_var = {'&bugurl' => sub{"$_[0]: ". 'http://'.$config{cgi_domain}.'/'. Debbugs::CGI::bug_links(bug=>$_[0], links_only => 1, ); } }; return fill_in_template(template => $template, variables => $variables, hole_var => $hole_var, ); } =head2 message_body_template message_body_template('mail/ack',{ref=>'foo'}); Creates a message body using a template =cut sub message_body_template{ my ($template,$extra_var) = @_; $extra_var ||={}; my $body = fill_template($template,$extra_var); return fill_template('mail/message_body', {%{$extra_var}, body => $body, }, ); } sub sendhelp { if ($control) { &sendtxthelpraw("bug-maint-mailcontrol.txt","instructions for control\@$gEmailDomain") } else { &sendtxthelpraw("bug-log-mailserver.txt","instructions for request\@$gEmailDomain"); } } #sub unimplemented { # print {$transcript} "Sorry, command $_[0] not yet implemented.\n\n"; #} our %checkmatch_values; sub checkmatch { my ($string,$mvarname,$svarvalue,@newmergelist) = @_; my ($mvarvalue); if (@newmergelist) { $mvarvalue = $checkmatch_values{$mvarname}; print {$transcript} "D| checkmatch \`$string' /$mvarname/$mvarvalue/$svarvalue/\n" if $dl; $mismatch .= "Values for \`$string' don't match:\n". " #$newmergelist[0] has \`$mvarvalue';\n". " #$ref has \`$svarvalue'\n" if $mvarvalue ne $svarvalue; } else { print {$transcript} "D| setupmatch \`$string' /$mvarname/$svarvalue/\n" if $dl; $checkmatch_values{$mvarname} = $svarvalue; } } sub checkpkglimit { if (keys %limit_pkgs and not defined $limit_pkgs{$data->{package}}) { print {$transcript} "$gBug number $ref belongs to package $data->{package}, skipping.\n\n"; $errors++; return 0; } return 1; } sub manipset { my $list = shift; my $elt = shift; my $add = shift; my %h = map { $_ => 1 } split ' ', $list; if ($add) { $h{$elt}=1; } else { delete $h{$elt}; } return join ' ', sort keys %h; } # High-level bug manipulation calls # Do announcements themselves # # Possible calling sequences: # setbug (returns 0) # # setbug (returns 1) # &transcript(something) # nochangebug # # setbug (returns 1) # $action= (something) # do { # (modify s_* variables) # } while (getnextbug); our $manybugs; sub nochangebug { &dlen("nochangebug"); $state eq 'single' || $state eq 'multiple' || die "$state ?"; &cancelbug; &endmerge if $manybugs; $state= 'idle'; &dlex("nochangebug"); } our $sref; our @thisbugmergelist; sub setbug { &dlen("setbug $ref"); if ($ref =~ m/^-\d+/) { if (!defined $clonebugs{$ref}) { ¬foundbug; &dlex("setbug => noclone"); return 0; } $ref = $clonebugs{$ref}; } $state eq 'idle' || die "$state ?"; if (!&getbug) { ¬foundbug; &dlex("setbug => 0s"); return 0; } if (!&checkpkglimit) { &cancelbug; return 0; } @thisbugmergelist= split(/ /,$data->{mergedwith}); if (!@thisbugmergelist) { &foundbug; $manybugs= 0; $state= 'single'; $sref=$ref; &dlex("setbug => 1s"); return 1; } &cancelbug; &getmerge; $manybugs= 1; if (!&getbug) { ¬foundbug; &endmerge; &dlex("setbug => 0mc"); return 0; } &foundbug; $state= 'multiple'; $sref=$ref; &dlex("setbug => 1m"); return 1; } sub getnextbug { &dlen("getnextbug"); $state eq 'single' || $state eq 'multiple' || die "$state ?"; &savebug; if (!$manybugs || !@thisbugmergelist) { length($action) || die; print {$transcript} "$action\n$extramessage\n"; &endmerge if $manybugs; $state= 'idle'; &dlex("getnextbug => 0"); return 0; } $ref= shift(@thisbugmergelist); &getbug || die "bug $ref disappeared"; &foundbug; &dlex("getnextbug => 1"); return 1; } # Low-level bug-manipulation calls # Do no announcements # # getbug (returns 0) # # getbug (returns 1) # cancelbug # # getmerge # $action= (something) # getbug (returns 1) # savebug/cancelbug # getbug (returns 1) # savebug/cancelbug # [getbug (returns 0)] # &transcript("$action\n\n") # endmerge sub notfoundbug { print {$transcript} "$gBug number $ref not found. (Is it archived?)\n\n"; } sub foundbug { print {$transcript} "$gBug#$ref: $data->{subject}\n"; } sub getmerge { &dlen("getmerge"); $mergelowstate eq 'idle' || die "$mergelowstate ?"; &filelock('lock/merge'); $mergelowstate='locked'; &dlex("getmerge"); } sub endmerge { &dlen("endmerge"); $mergelowstate eq 'locked' || die "$mergelowstate ?"; &unfilelock; $mergelowstate='idle'; &dlex("endmerge"); } sub getbug { &dlen("getbug $ref"); $lowstate eq 'idle' || die "$state ?"; # Only use unmerged bugs here if (($data = &lockreadbug($ref,'db-h'))) { $sref= $ref; $lowstate= "open"; &dlex("getbug => 1"); $extramessage=''; return 1; } $lowstate= 'idle'; &dlex("getbug => 0"); return 0; } sub cancelbug { &dlen("cancelbug"); $lowstate eq 'open' || die "$state ?"; &unfilelock; $lowstate= 'idle'; &dlex("cancelbug"); } sub savebug { &dlen("savebug $ref"); $lowstate eq 'open' || die "$lowstate ?"; length($action) || die; $ref == $sref || die "read $sref but saving $ref ?"; append_action_to_log(bug => $ref, action => $action, requester => $header{from}, request_addr => $controlrequestaddr, message => \@log, get_lock => 0, ); unlockwritebug($ref, $data); $lowstate= "idle"; &dlex("savebug"); } sub dlen { return if !$dl; print {$transcript} "C> @_ ($state $lowstate $mergelowstate)\n"; } sub dlex { return if !$dl; print {$transcript} "R> @_ ($state $lowstate $mergelowstate)\n"; } sub urlsanit { my $url = shift; $url =~ s/%/%25/g; $url =~ s/\+/%2b/g; my %saniarray = ('<','lt', '>','gt', '&','amp', '"','quot'); $url =~ s/([<>&"])/\&$saniarray{$1};/g; return $url; } sub sendlynxdoc { &sendlynxdocraw; print {$transcript} "\n"; $ok++; } sub sendtxthelp { &sendtxthelpraw; print {$transcript} "\n"; $ok++; } our $doc; sub sendtxthelpraw { my ($relpath,$description) = @_; $doc=''; if (not -e "$gDocDir/$relpath") { print {$transcript} "Unfortunatly, the help text doesn't exist, so it wasn't sent.\n"; warn "Help text $gDocDir/$relpath not found"; return; } open(D,"$gDocDir/$relpath") || die "open doc file $relpath: $!"; while() { $doc.=$_; } close(D); print {$transcript} "Sending $description in separate message.\n"; &sendmailmessage(< Precedence: bulk X-$gProject-PR-Message: doc-text $relpath END $ok++; } sub sendlynxdocraw { my ($relpath,$description) = @_; $doc=''; open(L,"lynx -nolist -dump http://$gCGIDomain/\Q$relpath\E 2>&1 |") || die "fork for lynx: $!"; while() { $doc.=$_; } $!=0; close(L); if ($? == 255 && $doc =~ m/^\n*lynx: Can\'t access start file/) { print {$transcript} "Information ($description) is not available -\n". "perhaps the $gBug does not exist or is not on the WWW yet.\n"; $ok++; } elsif ($?) { print {$transcript} "Error getting $description (code $? $!):\n$doc\n"; } else { print {$transcript} "Sending $description.\n"; &sendmailmessage(< Precedence: bulk X-$gProject-PR-Message: doc-html $relpath END $ok++; } } sub sendinfo { my ($wherefrom,$path,$description) = @_; if ($wherefrom eq "ftp.d.o") { $doc = `lynx -nolist -dump http://ftp.debian.org/debian/indices/$path.gz 2>&1 | gunzip -cf` or die "fork for lynx/gunzip: $!"; $! = 0; if ($? == 255 && $doc =~ m/^\n*lynx: Can\'t access start file/) { print {$transcript} "$description is not available.\n"; $ok++; return; } elsif ($?) { print {$transcript} "Error getting $description (code $? $!):\n$doc\n"; return; } } elsif ($wherefrom eq "local") { open P, "$path"; $doc = do { local $/;

}; close P; } else { print {$transcript} "internal errror: info files location unknown.\n"; $ok++; return; } print {$transcript} "Sending $description.\n"; &sendmailmessage(< Precedence: bulk X-$gProject-PR-Message: getinfo $description follows: END $ok++; print {$transcript} "\n"; }