1 # This module is part of debbugs, and is released
2 # under the terms of the GPL version 2, or any later
3 # version at your option.
4 # See the file README and COPYING for more information.
6 # [Other people have contributed to this file; their copyrights should
8 # Copyright 2007 by Don Armstrong <don@donarmstrong.com>.
10 package Debbugs::Control;
14 Debbugs::Control -- Routines for modifying the state of bugs
23 This module is an abstraction of a lot of functions which originally
24 were only present in service.in, but as time has gone on needed to be
25 called from elsewhere.
27 All of the public functions take the following options:
31 =item debug -- scalar reference to which debbuging information is
34 =item transcript -- scalar reference to which transcript information
37 =item affected_bugs -- hashref which is updated with bugs affected by
43 Functions which should (probably) append to the .log file take the
48 =item requester -- Email address of the individual who requested the change
50 =item request_addr -- Address to which the request was sent
52 =item location -- Optional location; currently ignored but may be
53 supported in the future for updating archived bugs upon archival
55 =item message -- The original message which caused the action to be taken
57 =item append_log -- Whether or not to append information to the log.
61 B<append_log> (for most functions) is a special option. When set to
62 false, no appending to the log is done at all. When it is not present,
63 the above information is faked, and appended to the log file. When it
64 is true, the above options must be present, and their values are used.
67 =head1 GENERAL FUNCTIONS
73 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
74 use base qw(Exporter);
78 $DEBUG = 0 unless defined $DEBUG;
81 %EXPORT_TAGS = (summary => [qw(summary)],
83 archive => [qw(bug_archive bug_unarchive),
85 log => [qw(append_action_to_log),
89 Exporter::export_ok_tags(keys %EXPORT_TAGS);
90 $EXPORT_TAGS{all} = [@EXPORT_OK];
93 use Debbugs::Config qw(:config);
94 use Debbugs::Common qw(:lock buglog :misc get_hashname);
95 use Debbugs::Status qw(bug_archiveable :read :hook writebug);
96 use Debbugs::CGI qw(html_escape);
97 use Debbugs::Log qw(:misc);
98 use Debbugs::Recipients qw(:add);
100 use Params::Validate qw(validate_with :types);
101 use File::Path qw(mkpath);
104 use Debbugs::Text qw(:templates);
106 use Debbugs::Mail qw(rfc822_date);
108 use POSIX qw(strftime);
112 # These are a set of options which are common to all of these functions
114 my %common_options = (debug => {type => SCALARREF|HANDLE,
117 transcript => {type => SCALARREF|HANDLE,
120 affected_bugs => {type => HASHREF,
123 affected_packages => {type => HASHREF,
126 recipients => {type => HASHREF,
129 limit => {type => HASHREF,
135 my %append_action_options =
136 (action => {type => SCALAR,
139 requester => {type => SCALAR,
142 request_addr => {type => SCALAR,
145 location => {type => SCALAR,
148 message => {type => SCALAR|ARRAYREF,
151 append_log => {type => BOOLEAN,
153 depends => [qw(requester request_addr),
160 # this is just a generic stub for Debbugs::Control functions.
162 # my %param = validate_with(params => \@_,
163 # spec => {bug => {type => SCALAR,
164 # regex => qr/^\d+$/,
166 # # specific options here
168 # %append_action_options,
173 # local $SIG{__DIE__} = sub {
175 # for (1..$locks) { unfilelock(); }
179 # my ($debug,$transcript) = __handle_debug_transcript(%param);
181 # ($locks, @data) = lock_read_all_merged_bugs($param{bug});
182 # __handle_affected_packages(data => \@data,%param);
183 # add_recipients(data => \@data,
184 # recipients => $param{recipients}
188 =head1 SUMMARY FUNCTIONS
194 transcript => $transcript,
195 ($dl > 0 ? (debug => $transcript):()),
196 requester => $header{from},
197 request_addr => $controlrequestaddr,
199 affected_packages => \%affected_packages,
200 recipients => \%recipients,
206 print {$transcript} "Failed to mark $ref with summary foo: $@";
209 Handles all setting of summary fields
211 If summary is undef, unsets the summary
213 If summary is 0, sets the summary to the first paragraph contained in
216 If summary is numeric, sets the summary to the message specified.
223 my %param = validate_with(params => \@_,
224 spec => {bug => {type => SCALAR,
227 # specific options here
228 summary => {type => SCALAR|UNDEF,
232 %append_action_options,
235 croak "summary must be numeric or undef" if
236 defined $param{summary} and not $param{summary} =~ /^\d+$/;
239 local $SIG{__DIE__} = sub {
241 for (1..$locks) { unfilelock(); }
245 my ($debug,$transcript) = __handle_debug_transcript(%param);
247 ($locks, @data) = lock_read_all_merged_bugs($param{bug});
248 __handle_affected_packages(data => \@data,%param);
249 add_recipients(data => \@data,
250 recipients => $param{recipients}
252 # figure out the log that we're going to use
254 my $summary_msg = '';
256 if (not defined $param{summary}) {
258 print {$debug} "Removing summary fields";
259 $action = 'Removed summary';
263 my @records = Debbugs::Log::read_log_records(bug_num => $param{bug});
264 if ($param{summary} == 0) {
266 $summary_msg = @records + 1;
269 if (($param{summary} - 1 ) > $#records) {
270 die "Message number '$param{summary}' exceeds the maximum message '$#records'";
272 my $record = $records[($param{summary} - 1 )];
273 if ($record->{type} !~ /incoming-recv|recips/) {
274 die "Message number '$param{summary}' is a invalid message type '$record->{type}'";
276 $summary_msg = $param{summary};
277 $log = [$record->{text}];
279 my $p_o = Debbugs::MIME::parse(join('',@{$log}));
280 my $body = $p_o->{body};
281 my $in_pseudoheaders = 0;
283 # walk through body until we get non-blank lines
284 for my $line (@{$body}) {
285 if ($line =~ /^\s*$/) {
286 if (length $paragraph) {
289 $in_pseudoheaders = 0;
292 # skip a paragraph if it looks like it's control or
294 if ($line =~ m{^\s*(?:(?:Package|Source|Version)\:| #pseudo headers
295 (?:package|(?:no|)owner|severity|tag|summary| #control
296 reopen|close|(?:not|)(?:fixed|found)|clone|
297 (?:force|)merge|user(?:category|tag|)
300 if (not length $paragraph) {
301 print {$debug} "Found control/pseudo-headers and skiping them\n";
302 $in_pseudoheaders = 1;
306 next if $in_pseudoheaders;
309 print {$debug} "Summary is going to be '$paragraph'\n";
310 $summary = $paragraph;
311 $summary =~ s/[\n\r]//g;
312 if (not length $summary) {
313 die "Unable to find summary message to use";
316 for my $data (@data) {
317 print {$debug} "Going to change summary";
318 if (length $summary) {
319 if (length $data->{summary}) {
320 $action = "Summary replaced with message bug $param{bug} message $summary_msg";
323 $action = "Summary recorded from message bug $param{bug} message $summary_msg";
326 $data->{summary} = $summary;
327 append_action_to_log(bug => $data->{bug_num},
329 __return_append_to_log_options(
334 if not exists $param{append_log} or $param{append_log};
335 writebug($data->{bug_num},$data);
336 print {$transcript} "$action\n";
337 add_recipients(data => $data,
338 recipients => $param{recipients},
342 for (1..$locks) { unfilelock(); }
350 =head1 OWNER FUNCTIONS
356 transcript => $transcript,
357 ($dl > 0 ? (debug => $transcript):()),
358 requester => $header{from},
359 request_addr => $controlrequestaddr,
361 recipients => \%recipients,
367 print {$transcript} "Failed to mark $ref as having an owner: $@";
370 Handles all setting of the owner field; given an owner of undef or of
371 no length, indicates that a bug is not owned by anyone.
376 my %param = validate_with(params => \@_,
377 spec => {bug => {type => SCALAR,
380 owner => {type => SCALAR|UNDEF,
383 %append_action_options,
388 local $SIG{__DIE__} = sub {
390 for (1..$locks) { unfilelock(); }
394 my ($debug,$transcript) = __handle_debug_transcript(%param);
396 ($locks, @data) = lock_read_all_merged_bugs($param{bug});
397 __handle_affected_packages(data => \@data,%param);
398 @data and defined $data[0] or die "No bug found for $param{bug}";
399 add_recipients(data => \@data,
400 recipients => $param{recipients}
403 for my $data (@data) {
404 print {$debug} "Going to change owner to '".(defined $param{owner}?$param{owner}:'(going to unset it)')."'\n";
405 print {$debug} "Owner is currently '$data->{owner}' for bug $data->{bug_num}\n";
406 if (not defined $param{owner} or not length $param{owner}) {
408 $action = "Removed annotation that $config{bug} was owned by " .
412 if (length $data->{owner}) {
413 $action = "Owner changed from $data->{owner} to $param{owner}.";
416 $action = "Owner recorded as $param{owner}."
419 $data->{owner} = $param{owner};
420 append_action_to_log(bug => $data->{bug_num},
422 __return_append_to_log_options(
427 if not exists $param{append_log} or $param{append_log};
428 writebug($data->{bug_num},$data);
429 print {$transcript} "$action\n";
430 add_recipients(data => $data,
431 recipients => $param{recipients},
435 for (1..$locks) { unfilelock(); }
440 =head1 ARCHIVE FUNCTIONS
447 bug_archive(bug => $bug_num,
449 transcript => \$transcript,
454 transcript("Unable to archive $bug_num\n");
457 transcript($transcript);
460 This routine archives a bug
464 =item bug -- bug number
466 =item check_archiveable -- check wether a bug is archiveable before
467 archiving; defaults to 1
469 =item archive_unarchived -- whether to archive bugs which have not
470 previously been archived; defaults to 1. [Set to 0 when used from
473 =item ignore_time -- whether to ignore time constraints when archiving
474 a bug; defaults to 0.
481 my %param = validate_with(params => \@_,
482 spec => {bug => {type => SCALAR,
485 check_archiveable => {type => BOOLEAN,
488 archive_unarchived => {type => BOOLEAN,
491 ignore_time => {type => BOOLEAN,
495 %append_action_options,
500 local $SIG{__DIE__} = sub {
502 for (1..$locks) { unfilelock(); }
506 my $action = "$config{bug} archived.";
507 my ($debug,$transcript) = __handle_debug_transcript(%param);
508 if ($param{check_archiveable} and
509 not bug_archiveable(bug=>$param{bug},
510 ignore_time => $param{ignore_time},
512 print {$transcript} "Bug $param{bug} cannot be archived\n";
513 die "Bug $param{bug} cannot be archived";
515 print {$debug} "$param{bug} considering\n";
517 ($locks, @data) = lock_read_all_merged_bugs($param{bug});
518 __handle_affected_packages(data => \@data,%param);
519 print {$debug} "$param{bug} read $locks\n";
520 @data and defined $data[0] or die "No bug found for $param{bug}";
521 print {$debug} "$param{bug} read done\n";
523 if (not $param{archive_unarchived} and
524 not exists $data[0]{unarchived}
526 print {$transcript} "$param{bug} has not been archived previously\n";
527 die "$param{bug} has not been archived previously";
529 add_recipients(recipients => $param{recipients},
532 my @bugs = map {$_->{bug_num}} @data;
533 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
534 for my $bug (@bugs) {
535 if ($param{check_archiveable}) {
536 die "Bug $bug cannot be archived (but $param{bug} can?)"
537 unless bug_archiveable(bug=>$bug,
538 ignore_time => $param{ignore_time},
542 # If we get here, we can archive/remove this bug
543 print {$debug} "$param{bug} removing\n";
544 for my $bug (@bugs) {
545 #print "$param{bug} removing $bug\n" if $debug;
546 my $dir = get_hashname($bug);
547 # First indicate that this bug is being archived
548 append_action_to_log(bug => $bug,
550 __return_append_to_log_options(
555 if not exists $param{append_log} or $param{append_log};
556 my @files_to_remove = map {s#$config{spool_dir}/db-h/$dir/##; $_} glob("$config{spool_dir}/db-h/$dir/$bug.*");
557 if ($config{save_old_bugs}) {
558 mkpath("$config{spool_dir}/archive/$dir");
559 foreach my $file (@files_to_remove) {
560 link( "$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file" ) or
561 copy( "$config{spool_dir}/db-h/$dir/$file", "$config{spool_dir}/archive/$dir/$file" );
564 print {$transcript} "archived $bug to archive/$dir (from $param{bug})\n";
566 unlink(map {"$config{spool_dir}/db-h/$dir/$_"} @files_to_remove);
567 print {$transcript} "deleted $bug (from $param{bug})\n";
569 bughook_archive(@bugs);
570 if (exists $param{bugs_affected}) {
571 @{$param{bugs_affected}}{@bugs} = (1) x @bugs;
573 print {$debug} "$param{bug} unlocking $locks\n";
575 for (1..$locks) { unfilelock(); }
577 print {$debug} "$param{bug} unlocking done\n";
584 bug_unarchive(bug => $bug_num,
586 transcript => \$transcript,
591 transcript("Unable to archive bug: $bug_num");
593 transcript($transcript);
595 This routine unarchives a bug
600 my %param = validate_with(params => \@_,
601 spec => {bug => {type => SCALAR,
605 %append_action_options,
609 local $SIG{__DIE__} = sub {
611 for (1..$locks) { unfilelock(); }
615 my $action = "$config{bug} unarchived.";
616 my ($debug,$transcript) = __handle_debug_transcript(%param);
617 print {$debug} "$param{bug} considering\n";
619 ($locks, @data) = lock_read_all_merged_bugs($param{bug},'archive');
620 __handle_affected_packages(data => \@data,%param);
621 print {$debug} "$param{bug} read $locks\n";
622 if (not @data or not defined $data[0]) {
623 print {$transcript} "No bug found for $param{bug}\n";
624 die "No bug found for $param{bug}";
626 print {$debug} "$param{bug} read done\n";
627 my @bugs = map {$_->{bug_num}} @data;
628 print {$debug} "$param{bug} bugs ".join(' ',@bugs)."\n";
629 print {$debug} "$param{bug} unarchiving\n";
631 for my $bug (@bugs) {
632 print {$debug} "$param{bug} removing $bug\n";
633 my $dir = get_hashname($bug);
634 my @files_to_copy = map {s#$config{spool_dir}/archive/$dir/##; $_} glob("$config{spool_dir}/archive/$dir/$bug.*");
635 mkpath("archive/$dir");
636 foreach my $file (@files_to_copy) {
638 link( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
639 copy( "$config{spool_dir}/archive/$dir/$file", "$config{spool_dir}/db-h/$dir/$file" ) or
640 die "Unable to copy $config{spool_dir}/archive/$dir/$file to $config{spool_dir}/db-h/$dir/$file";
642 push @files_to_remove, map {"$config{spool_dir}/archive/$dir/$_"} @files_to_copy;
643 print {$transcript} "Unarchived $config{bug} $bug\n";
645 unlink(@files_to_remove) or die "Unable to unlink bugs";
646 # Indicate that this bug has been archived previously
647 for my $bug (@bugs) {
648 my $newdata = readbug($bug);
649 if (not defined $newdata) {
650 print {$transcript} "$config{bug} $bug disappeared!\n";
651 die "Bug $bug disappeared!";
653 $newdata->{unarchived} = time;
654 append_action_to_log(bug => $bug,
656 __return_append_to_log_options(
661 if not exists $param{append_log} or $param{append_log};
662 writebug($bug,$newdata);
663 add_recipients(recipients => $param{recipients},
667 print {$debug} "$param{bug} unlocking $locks\n";
669 for (1..$locks) { unfilelock(); };
671 if (exists $param{bugs_affected}) {
672 @{$param{bugs_affected}}{@bugs} = (1) x @bugs;
674 print {$debug} "$param{bug} unlocking done\n";
677 =head2 append_action_to_log
681 This should probably be moved to Debbugs::Log; have to think that out
686 sub append_action_to_log{
687 my %param = validate_with(params => \@_,
688 spec => {bug => {type => SCALAR,
691 action => {type => SCALAR,
693 requester => {type => SCALAR,
695 request_addr => {type => SCALAR,
697 location => {type => SCALAR,
700 message => {type => SCALAR|ARRAYREF,
702 get_lock => {type => BOOLEAN,
707 # Fix this to use $param{location}
708 my $log_location = buglog($param{bug});
709 die "Unable to find .log for $param{bug}"
710 if not defined $log_location;
711 if ($param{get_lock}) {
712 filelock("lock/$param{bug}");
714 my $log = IO::File->new(">>$log_location") or
715 die "Unable to open $log_location for appending: $!";
717 "<!-- time:".time." -->\n".
718 "<strong>".html_escape($param{action})."</strong>\n".
719 "Request was from <code>".html_escape($param{requester})."</code>\n".
720 "to <code>".html_escape($param{request_addr})."</code>. \n".
722 "\7\n",escape_log(make_list($param{message})),"\n\3\n"
723 or die "Unable to append to $log_location: $!";
724 close $log or die "Unable to close $log_location: $!";
725 if ($param{get_lock}) {
733 =head1 PRIVATE FUNCTIONS
735 =head2 __handle_affected_packages
737 __handle_affected_packages(affected_packages => {},
745 sub __handle_affected_packages{
746 my %param = validate_with(params => \@_,
747 spec => {%common_options,
748 data => {type => ARRAYREF|HASHREF
753 for my $data (make_list($param{data})) {
754 $param{affected_packages}{$data->{package}} = 1;
758 =head2 __handle_debug_transcript
760 my ($debug,$transcript) = __handle_debug_transcript(%param);
762 Returns a debug and transcript filehandle
767 sub __handle_debug_transcript{
768 my %param = validate_with(params => \@_,
769 spec => {%common_options},
772 my $debug = globify_scalar(exists $param{debug}?$param{debug}:undef);
773 my $transcript = globify_scalar(exists $param{transcript}?$param{transcript}:undef);
774 return ($debug,$transcript);
777 sub __return_append_to_log_options{
779 my $action = $param{action} if exists $param{action};
780 if (not exists $param{requester}) {
781 $param{requester} = $config{control_internal_requester};
783 if (not exists $param{request_addr}) {
784 $param{request_addr} = $config{control_internal_request_addr};
786 if (not exists $param{message}) {
787 my $date = rfc822_date();
788 $param{message} = fill_in_template(template => 'mail/fake_control_message',
789 variables => {request_addr => $param{request_addr},
790 requester => $param{requester},
796 if (not defined $action) {
797 carp "Undefined action!";
798 $action = "unknown action";
800 return (action => $action,
801 (map {exists $append_action_options{$_}?($_,$param{$_}):()}