import of rt 3.0.4
[freeside.git] / rt / bin / rt-commit-handler
diff --git a/rt/bin/rt-commit-handler b/rt/bin/rt-commit-handler
new file mode 100644 (file)
index 0000000..29e443e
--- /dev/null
@@ -0,0 +1,846 @@
+#!/usr/bin/perl -w
+# BEGIN LICENSE BLOCK
+# 
+# Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
+# 
+# (Except where explictly superceded by other copyright notices)
+# 
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+# 
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+# 
+# Unless otherwise specified, all modifications, corrections or
+# extensions to this work which alter its source code become the
+# property of Best Practical Solutions, LLC when submitted for
+# inclusion in the work.
+# 
+# 
+# END LICENSE BLOCK
+
+# {{{ Docs
+# -*-Perl-*-
+#
+#ident "@(#)ccvs/contrib:$Name:  $:$Id: rt-commit-handler,v 1.1 2003-07-15 13:16:15 ivan Exp $"
+#
+# Perl filter to handle the log messages from the checkin of files in multiple
+# directories.  This script will group the lists of files by log message, and
+# send one piece of mail per unique message, no matter how many files are
+# committed.
+
+=head1 NAME rt-commit-handler
+
+=head1 USAGE
+
+
+
+=head2 Regular use
+
+Stick the following in in CVSROOT/commitinfo
+
+ ALL     /opt/rt3/bin/rt-commit-handler --record-last-dir
+
+Stick the following  in CVSROOT/loginfo
+
+ ALL     /opt/rt3/bin/rt-commit-handler --cvs-root /pathtocvs/root --rt %{Vvts}
+
+=head2 Invocation (advanced use)
+
+rt-commit-handler --cvs-root /path/to/cvs/root [-d] [-D] [-r] [-M module] \
+                [[-m mailto] ...] [[-R replyto] ...] [-f logfile] 
+
+
+       -d              - turn on debugging
+       -m mailto       - send mail to "mailto" (multiple)
+       -R replyto      - set the "Reply-To:" to "replyto" (multiple)
+       -M modulename   - set module name to "modulename"
+       -f logfile      - write commit messages to logfile too
+       -D              - generate diff commands
+        --rt              - invoke RT commit handler
+        --cvs-root       - specify your CVS root 
+
+        --record-last-dir -  Record the last directory with changes in
+                             pre-commit (commitinfo) mode
+
+
+=cut
+
+# }}}
+
+use strict;
+use Carp;
+use Getopt::Long;
+use Text::Wrap;
+use Digest::MD5;
+use MIME::Entity;
+
+use lib ("/opt/rt3/lib", "/opt/rt3/local/lib");
+
+use RT::Interface::CLI qw(CleanEnv GetCurrentUser GetMessageContent loc);
+
+use vars
+  qw(@MAILER $TMPDIR $FILE_PREFIX $LASTDIR_FILE $HASH_FILE $VERSION_FILE             $MESSAGE_FILE $MAIL_FILE $DEBUG $MAILTO $REPLYTO $id $MODULE_NAME
+  $LOGIN $COMMITLOG $CVS_ROOT $RT_HANDLER);
+
+#Clean out all the nasties from the environment
+CleanEnv();
+
+#Load etc/config.pm and drop privs
+RT::LoadConfig();
+
+#Drop setgid permissions
+RT::DropSetGIDPermissions();
+
+# {{{ Variable setup
+$TMPDIR      = '/tmp';
+$FILE_PREFIX = $TMPDIR . '/#cvs.';
+
+# The root of your CVS install. we should get this from some smarter place.
+# It needs a trailing /
+
+$LASTDIR_FILE = $FILE_PREFIX . "lastdir";
+$HASH_FILE    = $FILE_PREFIX . "hash";
+$VERSION_FILE = $FILE_PREFIX . "version";
+$MESSAGE_FILE = $FILE_PREFIX . "message";
+$MAIL_FILE    = $FILE_PREFIX . "mail";
+
+$DEBUG      = 0;
+$RT_HANDLER = 1;
+
+$MAILTO = '';
+
+my @files = ();
+my (@log_lines);
+my $do_diff = 0;
+my $id      = getpgrp();    # note, you *must* use a shell which does setpgrp()
+$LOGIN = getpwuid($<);
+
+# }}}
+
+die "User could not be found" unless ($LOGIN);
+
+# {{{ parse command line arguments (file list is seen as one arg)
+#
+while ( my $arg = shift @ARGV ) {
+
+    if ( $arg eq '-d' ) {
+        $DEBUG = 1;
+        warn "Debug turned on...\n";
+    }
+    elsif ( $arg =~ /^--record-last-dir$/i ) {
+        record_last_dir( $id, $ARGV[0] );
+        exit(0);
+    }
+    elsif ( $arg eq '-m' ) {
+        $MAILTO .= ", " if $MAILTO;
+        $MAILTO .= shift @ARGV;
+    }
+    elsif ( $arg eq '--rt' ) {
+        $RT_HANDLER = 1;
+    }
+    elsif ( $arg eq '-R' ) {
+        $REPLYTO .= ", " if $REPLYTO;
+        $REPLYTO .= shift @ARGV;
+    }
+    elsif ( $arg eq '-M' ) {
+        die ("too many '-M' args\n") if $MODULE_NAME;
+        $MODULE_NAME = shift @ARGV;
+    }
+    elsif ( $arg eq '--cvs-root' ) {
+        $CVS_ROOT = shift @ARGV;
+        $CVS_ROOT .= "/" unless ( $CVS_ROOT =~ /\/$/ );
+    }
+    elsif ( $arg eq '-f' ) {
+        die ("too many '-f' args\n") if $COMMITLOG;
+        $COMMITLOG = shift @ARGV;
+
+        # This is a disgusting hack to untaint $COMMITLOG if we're running from
+        # setgid cvs.
+        $COMMITLOG = untaint($COMMITLOG);
+    }
+    elsif ( $arg eq '-D' ) {
+        $do_diff = 1;
+    }
+    else {
+        @files = split ( ' ', $arg );
+        last;
+    }
+}
+
+# }}}
+
+$REPLYTO = $LOGIN unless ($REPLYTO);
+
+# for now, the first "file" is the repository directory being committed,
+# relative to the $CVSROOT location
+#
+my $dir = shift @files;
+
+# XXX there are some ugly assumptions in here about module names and
+# XXX directories relative to the $CVSROOT location -- really should
+# XXX read $CVSROOT/CVSROOT/modules, but that's not so easy to do, since
+# XXX we have to parse it backwards.
+#
+# XXX For now we set the `module' name to the top-level directory name.
+#
+unless ($MODULE_NAME) {
+    ($MODULE_NAME) = split ( '/', $dir, 2 );
+}
+
+if ($DEBUG) {
+    warn "module - ", $MODULE_NAME, "\n";
+    warn "dir    - ", $dir,         "\n";
+    warn "files  - ", join ( " ", @files ), "\n";
+    warn "id     - ", $id, "\n";
+}
+
+# {{{ Check for a new directory or an import command.
+
+#
+#    files[0] - "-"
+#    files[1] - "New"
+#    files[2] - "directory"
+#
+#    files[0] - "-"
+#    files[1] - "Imported"
+#    files[2] - "sources"
+#
+if ( $files[0] eq "-" ) {
+
+    #we just don't care about  New Directory notes
+    unless ( $files[1] eq "New" && $files[2] eq "directory" ) {
+
+        my @text = ();
+
+        push @text, build_header();
+        push @text, "";
+
+        while ( my $line = <STDIN> ) {
+            chop $line;    # Drop the newline
+            push @text, $line;
+        }
+
+        append_logfile( $COMMITLOG, @text ) if ($COMMITLOG);
+
+        mail_notification( $id, @text );
+    }
+
+    exit 0;
+}
+
+# }}}
+
+# {{{ Collect just the log message from stdin.
+#
+
+while ( my $line = <STDIN> ) {
+    chop $line;    # strip the newline
+    last if ( $line =~ /^Log Message:$/ );
+}
+while ( my $line = <STDIN> ) {
+    chop $line;    # strip the newline
+    $line =~ s/\s+$//;    # strip trailing white space
+    push @log_lines, $line;
+}
+
+my $md5 = Digest::MD5->new();
+foreach my $line (@log_lines) {
+    $md5->add( $line . "\n" );
+}
+my $hash = $md5->hexdigest();
+
+warn "hash = $hash\n" if ($DEBUG);
+
+if ( !-e "$MESSAGE_FILE.$id.$hash" ) {
+    append_logfile( "$HASH_FILE.$id",      $hash );
+    write_file( "$MESSAGE_FILE.$id.$hash", @log_lines );
+}
+
+# }}}
+
+# Spit out the information gathered in this pass.
+
+append_logfile( "$VERSION_FILE.$id.$hash", $dir . '/', @files );
+
+# {{{ Check whether this is the last directory.  If not, quit.
+
+warn "Checking current dir against last dir $LASTDIR_FILE.$id\n" if ($DEBUG);
+
+my @last_dir = read_file("$LASTDIR_FILE.$id");
+
+unless ($CVS_ROOT) {
+    die "No cvs root specified with --cvs-root. Can't continue.";
+}
+
+if ( $last_dir[0] ne $CVS_ROOT . $dir ) {
+    warn "Current directory $CVS_ROOT$dir is not last directory $last_dir[0].\n"
+      if ($DEBUG);
+    exit 0;
+}
+
+# }}}
+
+# {{{ End Of Commits!
+#
+
+# This is it.  The commits are all finished.  Lump everything together
+# into a single message, fire a copy off to the mailing list, and drop
+# it on the end of the Changes file.
+#
+
+#
+# Produce the final compilation of the log messages
+#
+
+my @hashes = read_file("$HASH_FILE.$id");
+my (@text);
+
+push @text, build_header();
+push @text, "";
+
+my ( @added_files, @modified_files, @removed_files );
+
+foreach my $hash (@hashes) {
+
+    # In case we're running setgid, make sure the hash file hasn't been hacked.
+    $hash =~ m/([a-z0-9]*)/ || die "*** Hacking attempt detected\n";
+    $hash = $1;
+
+    my @files     = read_file("$VERSION_FILE.$id.$hash");
+    my @log_lines = read_file("$MESSAGE_FILE.$id.$hash");
+
+    my $working_on_dir;    # gets set as we iterate through the files.
+    foreach my $file (@files) {
+
+        #If we've entered a new directory, make a note of that and remove the trailing /
+
+        if ( $file =~ s'\/$'' ) {
+            $working_on_dir = $file;
+            next;
+        }
+
+        my @file_entry = ( split ( ',', $file, 4 ), $working_on_dir );
+
+        # file_entry looks like ths:
+
+        # 0        1          2      3        4
+        # Old rev : new rev : tag:   file    :directory        
+        my $entry = {};
+        $entry->{'old'}  = $file_entry[0];
+        $entry->{'new'}  = $file_entry[1];
+        $entry->{'tag'}  = $file_entry[2];
+        $entry->{'file'} = $file_entry[3];
+        $entry->{'dir'}  = $file_entry[4];
+
+        if ( $file_entry[0] eq 'NONE' ) {
+            $entry->{'old'} = '0';
+            push @added_files, $entry;
+        }
+        elsif ( $file_entry[1] eq 'NONE' ) {
+            $entry->{'new'} = '0';
+            push @removed_files, $entry;
+        }
+        else {
+            push @modified_files, $entry;
+        }
+    }
+}
+
+# }}}
+
+# {{{ start building up the body
+
+# Strip leading and trailing blank lines from the log message.  Also
+# compress multiple blank lines in the body of the message down to a
+# single blank line.
+#
+
+my $blank = 1;
+@log_lines = map {
+    my $wasblank = $blank;
+    $blank = $_ eq '';
+    $blank && $wasblank ? () : $_;
+} @log_lines;
+
+pop @log_lines if $blank;
+
+@modified_files = order_and_summarize_diffs(@modified_files);
+@added_files    = order_and_summarize_diffs(@added_files);
+@removed_files  = order_and_summarize_diffs(@removed_files);
+
+push @text, "Modified Files:", format_lists(@modified_files)
+  if (@modified_files);
+
+push @text, "Added Files:", format_lists(@added_files) if (@added_files);
+
+push @text, "Removed Files:", format_lists(@removed_files) if (@removed_files);
+
+push @text, "", "Log Message", @log_lines if (@log_lines);
+
+push @text, "";
+
+if ($RT_HANDLER) {
+    rt_handler(
+        @log_lines,                             "\n",
+        loc("To generate a diff of this commit:\n"), "\n",
+        format_diffs( @modified_files, @added_files, @removed_files )
+    );
+}
+
+if ($COMMITLOG) {
+    append_logfile( $COMMITLOG, @text );
+}
+
+if ($do_diff) {
+    push @text, "";
+    push @text, loc("To generate a diff of this commit:");
+    push @text, format_diffs( @modified_files, @added_files, @removed_files );
+    push @text, "";
+}
+
+# }}}
+
+# {{{ Mail out the notification.
+
+mail_notification( $id, @text );
+
+# }}} 
+
+# {{{ clean up
+
+unless ($DEBUG) {
+    $hash = untaint($hash);
+    $id   = untaint($id);
+    unlink "$VERSION_FILE.$id.$hash";
+    unlink "$MESSAGE_FILE.$id.$hash";
+    unlink "$MAIL_FILE.$id";
+    unlink "$LASTDIR_FILE.$id";
+    unlink "$HASH_FILE.$id";
+}
+
+# }}}
+
+exit 0;
+
+# {{{ Subroutines
+#
+
+# {{{ append_logfile
+sub append_logfile {
+    my $filename = shift;
+    my (@lines) = @_;
+
+    $filename = untaint($filename);
+
+    open( FILE, ">>$filename" )
+      || die ("Cannot open file $filename for append.\n");
+    foreach my $line (@lines) {
+        print FILE $line . "\n";
+    }
+    close(FILE);
+}
+
+# }}}
+
+# {{{ write_file
+sub write_file {
+    my $filename = shift;
+    my (@lines) = @_;
+
+    $filename = untaint($filename);
+
+    open( FILE, ">$filename" )
+      || die ("Cannot open file $filename for write.\n");
+    foreach my $line (@lines) {
+        print FILE $line . "\n";
+    }
+    close(FILE);
+}
+
+# }}}
+
+# {{{ read_file
+sub read_file {
+    my $filename = shift;
+    my (@lines);
+
+    open( FILE, "<$filename" )
+      || die ("Cannot open file $filename for read.\n");
+    while ( my $line = <FILE> ) {
+        chop $line;
+        push @lines, $line;
+    }
+    close(FILE);
+
+    return (@lines);
+}
+
+# }}}
+
+# {{{ sub format_lists
+
+sub format_lists {
+    my @items = (@_);
+
+    my $files = "";
+    map {
+        $_->{'files'} && ( $files .= ' ' . join ( ' ', @{ $_->{'files'} } ) );
+    } @items;
+
+    my @lines = wrap( "\t", "\t\t", $files );
+    return (@lines);
+
+}
+
+# }}}
+
+# {{{ sub format_diffs
+
+sub format_diffs {
+    my @items = (@_);
+
+    my @lines;
+    foreach my $item (@items) {
+        next unless ( $item->{'files'} );
+        push ( @lines,
+            "cvs diff -r"
+              . $item->{'old'} . " -r"
+              . $item->{'new'} . " "
+              . join ( " ", @{ $item->{'files'} } ) . "\n" );
+
+    }
+
+    @lines = fill( "\t", "\t\t", @lines );
+
+    return (@lines);
+}
+
+# }}}
+
+# {{{ sub order_and_summarize_diffs {
+
+# takes an array of file items
+# returns a sorted array of fileset items, which are like file items, except they can have an array of files, rather than 
+# a singleton file.
+
+sub order_and_summarize_diffs {
+
+    my @files = (@_);
+
+    # Sort by tag, dir, file.
+    @files = sort {
+        $a->{'tag'} cmp $b->{'tag'}
+          || $a->{'dir'} cmp $b->{'dir'}
+          || $a->{'file'} cmp $b->{'file'};
+    } @files;
+
+    # Combine adjacent rows that are the same modulo the file name.
+
+    my @items = (undef);
+
+    foreach my $file (@files) {
+        if ( $#items == -1    #if it's empty
+            || ( !defined $items[-1]->{'old'}
+                || $items[-1]->{'old'} ne $file->{'old'} )
+            || ( !defined $items[-1]->{'new'}
+                || $items[-1]->{'new'} ne $file->{'new'} )
+            || ( !defined $items[-1]->{'tag'}
+                || $items[-1]->{'tag'} ne $file->{'tag'} ) )
+        {
+
+            push ( @items, $file );
+        }
+        push ( @{ $items[-1]->{'files'} },
+            $file->{'dir'} . "/" . $file->{'file'} );
+    }
+
+    return (@items);
+}
+
+# }}}
+
+# {{{ build_header
+
+sub build_header {
+    my $now    = gmtime;
+    my $header =
+      sprintf( "Module Name:\t%s\nCommitted By:\t%s\nDate:\t\t%s %s %s",
+        $MODULE_NAME, $LOGIN, substr( $now, 0, 19 ), "UTC",
+        substr( $now, 20, 4 ) );
+    return ($header);
+}
+
+# }}}
+
+# {{{ mail_notification
+sub mail_notification {
+    my $id = shift;
+    my (@text) = @_;
+    write_file( "$MAIL_FILE.$id", "From: " . $LOGIN,
+        "Subject: CVS commit: " . $MODULE_NAME, "To: " . $MAILTO,
+        "Reply-To: " . $REPLYTO,                "", "", @text );
+
+    my $entity = MIME::Entity->build(
+        From       => $LOGIN,
+        To         => $MAILTO,
+        Subject    => "CVS commit: " . $MODULE_NAME,
+        'Reply-To' => $REPLYTO,
+        Data       => join ( "\n", @text )
+    );
+    if ( $RT::MailCommand eq 'sendmailpipe' ) {
+        open( MAIL, "|$RT::SendmailPath $RT::SendmailArguments" )
+          || die "Couldn't send mail: " . $@ . "\n";
+        print MAIL $entity->as_string;
+        close(MAIL);
+    }
+    else {
+        $entity->send( $RT::MailCommand, $RT::MailParams );
+    }
+
+}
+
+# }}}
+
+# {{{ sub record_last_dir
+
+sub record_last_dir {
+    my $id  = shift;
+    my $dir = shift;
+
+    # make a note of this directory. later, we'll use this to 
+    # figure out if we've gone through the whole commit,
+    # for something that is a bad mockery of attomic commits.
+
+    warn "about to write $dir to $LASTDIR_FILE.$id" if ($DEBUG);
+
+    write_file( "$LASTDIR_FILE.$id", $dir );
+}
+
+# }}}
+
+# {{{ Get the RT stuff set up
+
+# {{{ sub rt_handler 
+
+sub rt_handler {
+    my (@LogMessage) = (@_);
+
+    #Connect to the database and get RT::SystemUser and RT::Nobody loaded
+    RT::Init;
+
+    require RT::Ticket;
+
+    #Get the current user all loaded
+    my $CurrentUser = GetCurrentUser();
+
+    if ( !$CurrentUser->Id ) {
+        print
+loc("No valid RT user found. RT cvs handler disengaged. Please consult your RT administrator.\n");
+        return;
+    }
+
+    my (@commands) = find_commands( \@LogMessage );
+
+    my ( @tickets, @errors );
+
+    # Get the list of tickets we're working with out of commands
+    grep { $_ =~ /^RT-Ticket:\s*(.*?)$/i && push ( @tickets, $1 ) } @commands;
+
+    my $message = new MIME::Entity;
+    $message->build(
+        From    => $CurrentUser->EmailAddress,
+        Subject => 'CVS Commit',
+        Data    => \@LogMessage
+    );
+
+    # {{{ comment or correspond, as needed
+
+    foreach my $ticket (@tickets) {
+        my $TicketObj = RT::Ticket->new($CurrentUser);
+        $TicketObj->Load($ticket);
+        my ( $id, $msg );
+        unless ( $TicketObj->Id ) {
+            push ( @errors,
+"Couldn't load ticket #$ticket. Not adding commit log to ticket history.\n"
+            );
+        }
+
+        if ( $LogMessage[0] =~ /^(comment|private)$/ ) {
+            ( $id, $msg ) = $TicketObj->Comment( MIMEObj => $message );
+
+        }
+        else {
+            ( $id, $msg ) = $TicketObj->Correspond( MIMEObj => $message );
+        }
+
+        push ( @errors, ">> Log message",
+            "Ticket #" . $TicketObj->Id . ": " . $msg );
+
+    }
+
+    # }}}
+
+    my ($reply) = ActOnPseudoHeaders( $CurrentUser, @commands );
+    print "$reply\n" if ($reply);
+    print join ( "\n", @errors );
+    print "\n";
+
+}
+
+# }}}
+
+# {{{ sub find_commands
+
+sub find_commands {
+    my $lines = shift;
+    my (@pseudoheaders);
+
+    while ( my $line = shift @{$lines} ) {
+        next if $line =~ /^\s*?$/;
+        if ( $line =~ /^RT-/i ) {
+
+            push ( @pseudoheaders, $line );
+        }
+
+        #If we find a line that's not a command, get out.
+        else {
+            unshift ( @{$lines}, $line );
+            last;
+        }
+    }
+
+    return (@pseudoheaders);
+
+}
+
+# }}}
+
+# {{{ sub ActOnPseudoHeaders
+
+=item ActOnPseudoHeaders $PseudoHeaders
+
+Takes a string of pseudo-headers, iterates through them and does what they tell it to.
+
+=cut
+
+sub ActOnPseudoHeaders {
+    my $CurrentUser = shift;
+    my (@actions) = (@_);
+
+    my $ResultsMessage = '';
+    my $Ticket         = RT::Ticket->new($CurrentUser);
+
+    foreach my $action (@actions) {
+        my ($val);
+        my $msg = '';
+
+        $ResultsMessage .= ">>> $action\n";
+
+        if ( $action =~ /^RT-(.*?):\s*(.*)$/i ) {
+            my $command = $1;
+            my $args    = $2;
+
+            if ( $command =~ /^ticket$/i ) {
+
+                $val = $Ticket->Load($args);
+                unless ($val) {
+                    $ResultsMessage .=
+                      loc("ERROR: Couldn't load ticket '[_1]': [_2].\n", $1, $msg);
+                      . loc("Aborting to avoid unintended ticket modifications.\n")
+                      . loc("The following commands were not proccessed:\n\n")
+                      . join ( "\n", @actions );
+                    return ($ResultsMessage);
+                }
+                $ResultsMessage .= loc("Ticket [_1] loaded\n", $Ticket->Id);
+            }
+            else {
+                unless ( $Ticket->Id ) {
+                    $ResultsMessage .= loc("No Ticket specified. Aborting ticket ")
+                      . loc("modifications\n\n")
+                      . loc("The following commands were not proccessed:\n\n")
+                      . join ( "\n", @actions );
+                    return ($ResultsMessage);
+                }
+
+                # Deal with the basics
+                if ( $command =~ /^(Subject|Owner|Status|Queue)$/i ) {
+                    my $method = 'Set' . ucfirst( lc($1) );
+                    ( $val, $msg ) = $Ticket->$method($args);
+                }
+
+                # Deal with the dates
+                elsif ( $command =~ /^(due|starts|started|resolved)$/i ) {
+                    my $method = 'Set' . ucfirst( lc($1) );
+                    my $date   = new RT::Date($CurrentUser);
+                    $date->Set( Format => 'unknown', Value => $args );
+                    ( $val, $msg ) = $Ticket->$method( $date->ISO );
+                }
+
+                # Deal with the watchers
+                elsif ( $command =~ /^(requestor|requestors|cc|admincc)$/i ) {
+                    my $operator = "+";
+                    my ($type);
+                    if ( $args =~ /^(\+|\-)(.*)$/ ) {
+                        $operator = $1;
+                        $args     = $2;
+                    }
+                    $type = 'Requestor' if ( $command =~ /^requestor/i );
+                    $type = 'Cc'        if ( $command =~ /^cc/i );
+                    $type = 'AdminCc'   if ( $command =~ /^admincc/i );
+
+                       my $user = RT::User->new($CurrentUser);
+                    $user->Load($args);
+
+                    if ($operator eq '+') {
+                        ($val, $msg) = $Ticket->AddWatcher( Type => $type,
+                                                            PrincipalId => $user->PrincipalId);
+                    } elsif ($operator eq '-') {
+                        ($val, $msg) = $Ticket->DeleteWatcher( Type => $type,
+                                                               PrincipalId => $user->PrincipalId);
+                    }
+
+            }
+            $ResultsMessage .= $msg . "\n";
+        }
+
+    }
+    return ($ResultsMessage);
+
+}
+
+# }}}
+
+# {{{ sub untaint 
+sub untaint {
+    my $val = shift;
+
+    if ( $val =~ /^([-\#\/\w.]+)$/ ) {
+        $val = $1;    # $data now untainted
+    }
+    else {
+        die loc("Bad data in [_1]", $val);    # log this somewhere
+    }
+    return ($val);
+}
+
+# }}}
+
+=head1 AUTHOR
+
+
+
+  rt-commit-handler is a rewritten version of the NetBSD commit handler,
+  which was placed in the public domain by Charles Hannum. It bore the following
+  authors statement:
+
+ Contributed by David Hampton <hampton@cisco.com>
+ Hacked greatly by Greg A. Woods <woods@planix.com>
+ Rewritten by Charles M. Hannum <mycroft@netbsd.org>
+
+=cut
+