# -*- Mode: perl; tab-width: 4; indent-tabs-mode: nil; -*- # vim: syntax=perl ################################ # Bugzilla Module # ################################ package BotModules::Bugzilla; use vars qw(@ISA); @ISA = qw(BotModules); use XML::LibXML; use Fcntl qw(:DEFAULT :flock); use File::Basename; # For parsing bugmail.log records. Must be the same as # FIELD_SEPARATOR in bugmail.pl. use constant FIELD_SEPARATOR => '::::'; # The log file that we read to report bug changes. # This will be put in the directory returned by dirname($0). use constant BUGMAIL_LOG => 'BotModules/.bugmail.log'; 1; # there is a minor error in this module: bugsHistory->$target->$bug is # accessed even when bugsHistory->$target doesn't yet exist. XXX # This is ported straight from techbot, so some of the code is a little convoluted. So sue me. I was lazy. sub Initialise { my $self = shift; my $retval = $self->SUPER::Initialise(@_); my ($throw_away) = $self->GetBugLog(); $throw_away->truncate(0) if $throw_away; $throw_away->close() if $throw_away; return $retval; } # RegisterConfig - Called when initialised, should call registerVariables sub RegisterConfig { my $self = shift; $self->SUPER::RegisterConfig(@_); $self->registerVariables( # [ name, save?, settable? ] ['bugsURI', 1, 1, 'https://bugzilla.mozilla.org/'], ['bugsDWIMQueryDefault', 1, 1, 'short_desc_type=substring&short_desc='], ['bugsDWIMQueryChannelDefault', 1, 1, {}], ['bugsHistory', 0, 0, {}], ['backoffTime', 1, 1, 120], ['ignoreCommentsTo', 1, 1, ['']], ['ignoreCommentsFrom', 1, 1, ['|']], ['mailIgnore', 1, 1, []], ['skipPrefixFor', 1, 1, []], # The keys for productReportChannels can be in the form of 'Product' # or 'Product::::Component'. The value is a comma-separated list of # channel names. ['productReportChannels', 1, 1, {}], # The fields that you want notifications about. ['reportFields', 1, 1, ['Resolution', 'Flag', 'Attachment Flag', 'NewBug', 'NewAttach']], # Except in these products, you don't want notifications about # certain fields (key is product name, value is comma-separated # list of fields). ['productMuteFields', 1, 1, {}], # And in these channels, you don't want notifications about certain # fields (the key is the channel name and the value is a # comma-separated list of fields). ['channelMuteFields', 1, 1, {}], # How frequently we check for new bugmail we've received, in seconds. ['updateDelay', 1, 1, 10], # List of products for which component of new bugs is reported instead # of only the product. Can also restrict to specific components # by using Product::::Component syntax and always report components # by using 'all'. ['reportComponent', 1, 1, ['all']], ['mutes', 1, 1, ''], # "channel channel channel" # Optionally skip fetching the bug details for automatic notifications ['reportBugDetails', 1, 1, 1] ); } sub Help { my $self = shift; my ($event) = @_; my %commands = ( '' => 'The Bugzilla module provides an interface to the bugzilla bug database. It will spot anyone mentioning bugs, too, and report on what they are. For example if someone says \'I think that\'s a dup of bug 5693, the :hover thing\', then this module will display information about bug 5693.', 'bug' => 'Fetches a summary of bugs from bugzilla. Expert syntax: \'bugzilla [bugnumber[,]]*[&bugzillaparameter=value]*\', bug_status: UNCONFIRMED|NEW|ASSIGNED|REOPENED; *type*=substring|; bugtype: include|exclude; order: Assignee|; chfield[from|to|value] short_desc\' long_desc\' status_whiteboard\' bug_file_loc\' keywords\'; \'_type; email[|type][1|2] [reporter|qa_contact|assigned_to|cc]', 'bug-total' => 'Same as bug (which see) but only displays the total line.', 'bugs' => q{A simple DWIM search. Not very clever. ;-)} . q{ Syntax: ' bugs' e.g. 'mozbot bugs'.}, 'ignore' => q{Causes the bot to stop reporting all bug changes} . q{ made by a particular user in the current channel.} . q{ Syntax: 'ignore ' }, 'unignore' => q{Causes the bot to un-ignore a previously ignored} . q{ user. See 'ignore'} . q{ for more details.}, ); if ($self->isAdmin($event)) { $commands{'mute'} = 'Disable watching for bug numbers in a channel. Syntax: mute bugzilla in '; $commands{'unmute'} = 'Enable watching for bug numbers in a channel. Syntax: unmute bugzilla in '; } return \%commands; } # Schedule - called when bot connects to a server, to install any schedulers # use $self->schedule($event, $delay, $times, $data) # where $times is 1 for a single event, -1 for recurring events, # and a +ve number for an event that occurs that many times. sub Schedule { my $self = shift; my ($event) = @_; $self->schedule($event, \$self->{'updateDelay'}, -1, 'Bugzilla-BugMail'); return $self->SUPER::Schedule($event); } sub Scheduled { my $self = shift; my ($event, @data) = @_; if ($data[0] eq 'Bugzilla-BugMail') { $self->CheckForBugMail($event); } else { return $self->SUPER::Scheduled($event, @data); } return 0; } sub Told { my $self = shift; my ($event, $message) = @_; if ($message =~ /^\s*ignore (.+)[?!.\s]*$/) { my $user = $1; # If we aren't already ignoring them... if (!grep($_ eq $user, @{$self->{'mailIgnore'}})) { push (@{$self->{'mailIgnore'}}, $user); $self->saveConfig(); $self->say($event, "$event->{'from'}: OK, ignoring changes produced by $user."); } else { $self->say($event, "$event->{'from'}: $user is already being ignored."); } } elsif ($message =~ /^\s*unignore (.+)[?!.\s]*$/) { my $user = $1; my %ignoredUsers = map { $_ => 1 } @{$self->{'mailIgnore'}}; # If we are already ignoring them... if ($ignoredUsers{$user}) { delete $ignoredUsers{$user}; $self->{'mailIgnore'} = [keys %ignoredUsers]; $self->saveConfig(); $self->say($event, "$event->{'from'}: OK, $user is no longer being ignored."); } else { $self->say($event, "$event->{'from'}: $user wasn't being ignored."); } } elsif ($message =~ m/^ \s* # some optional whitespace (?:please\s+)? # an optional "please", followed optionally by either: (?: (?:could\s+you\s+)? # 1. an optional "could you", (?:please\s+)? # another optional "please", show\s+me\s+ | # and the text "show me" what\s+is\s+ | # 2. the text "what is" what\'s\s+ )? # 3. or the text "what's" bug (?:\s*id)?s? [\#\s]+ # a variant on "bug", "bug id", "bugids", etc ([0-9].*?| # a query string, either a number followed by some optional text, or &.+?) # a query string, starting with a &. (?:\s+please)? # followed by yet another optional "please" [?!.\s]* # ending with some optional punctuation $/osix) { my $target = $event->{'target'}; my $bug = $1; # Single bugs use xml.cgi, because then we get error messages if ($bug =~ m/^\d+$/) { $self->FetchBug($event, $bug, 'bug', {'sayAlways' => 1}); } else { $self->FetchBug($event, $bug, 'bugs', {'sayAlways' => 1}); } $self->{'bugsHistory'}->{$target}->{$bug} = $event->{'time'} if $bug =~ m/^\d+$/os; } elsif ($message =~ m/^\s*bug-?total\s+(.+?)\s*$/osi) { $self->FetchBug($event, $1, 'total'); } elsif ($self->isAdmin($event)) { if ($message =~ m/^\s*mute\s+bugzilla\s+in\s+(\S+?)\s*$/osi) { $self->{'mutes'} .= " $1"; $self->saveConfig(); $self->say($event, "$event->{'from'}: Watching for bug numbers disabled in channel $1."); } elsif ($message =~ m/^\s*unmute\s+bugzilla\s+in\s+(\S+)\s*$/osi) { my %mutedChannels = map { $_ => 1 } split(/ /o, $self->{'mutes'}); delete($mutedChannels{$1}); # get rid of any mentions of that channel $self->{'mutes'} = join(' ', keys(%mutedChannels)); $self->saveConfig(); $self->say($event, "$event->{'from'}: Watching for bug numbers reenabled in channel $1."); } else { return $self->SUPER::Told(@_); } } else { return $self->SUPER::Told(@_); } return 0; # dealt with it... } sub CheckForBugs { my $self = shift; my ($event, $message) = @_; if ((($event->{'channel'} eq '') or # either it was /msg'ed, or ($self->{'mutes'} !~ m/^(?:.*\s|)\Q$event->{'channel'}\E(?:|\s.*)$/si)) and # it was sent on a channel in which we aren't muted (not $self->ignoringCommentsFrom($event->{'from'})) and # we aren't ignoring them (not $self->ignoringCommentsTo($message))) { # and they aren't talking to someone we need to ignore my $rest = $message; my $bugsFound = 0; my $bugsToFetch = ''; my $bug; my $skipURI; do { if ($rest =~ m/ (?:^| # either the start of the string []\s,.;:\\\/=?!()<>{}[-]) # or some punctuation bug [\s\#]* ([0-9]+) # followed a string similar to "bug # 123" (put the number in $1) (?:[]\s,.;:\\\/=?!()<>{}[-]+ # followed optionally by some punctuation, (.*))?$/osix) { # and everything else (which we put in $2) $bug = $1; $skipURI = 0; $rest = $2; } elsif ($rest =~ m/\Q$self->{'bugsURI'}\Eshow_bug.cgi\?id=([0-9]+)(?:[^0-9&](.*))?$/si) { $bug = $1; $skipURI = 1; $rest = $2; } else { $bug = undef; } if (defined($bug)) { $self->debug("Noticed someone mention bug $bug -- investigating..."); $bugsToFetch .= "$bug "; $bugsFound++; } } while (defined($bug)); if ($bugsToFetch ne '') { $self->FetchBug($event, $bugsToFetch, 'bug', {'skipURI' => $skipURI, 'skipZaroo' =>1}); } return $bugsFound; } return 0; } sub Heard { my $self = shift; my ($event, $message) = @_; unless ($self->CheckForBugs($event, $message)) { return $self->SUPER::Heard(@_); } return 0; # we've dealt with it, no need to do anything else. } sub Baffled { my $self = shift; my ($event, $message) = @_; if ($message =~ m/^\s*(...+?)\s+bugs\s*$/osi) { my $target = $event->{'target'}; $self->FetchBug($event, $1, 'dwim'); } else { return $self->SUPER::Baffled(@_); } return 0; } sub Felt { my $self = shift; my ($event, $message) = @_; unless ($self->CheckForBugs($event, $message)) { return $self->SUPER::Felt(@_); } return 0; # we've dealt with it, no need to do anything else. } sub Saw { my $self = shift; my ($event, $message) = @_; unless ($self->CheckForBugs($event, $message)) { return $self->SUPER::Saw(@_); } return 0; # we've dealt with it, no need to do anything else. } sub FetchBug { my $self = shift; my ($event, $bugParams, $subtype, $params) = @_; my $skipURI = exists($params->{'skipURI'}) ? $params->{'skipURI'} : 0; my $skipZaroo = exists($params->{'skipZaroo'}) ? $params->{'skipZaroo'} : 0; my $sayAlways = exists($params->{'sayAlways'}) ? $params->{'sayAlways'} : 0; my $uri; my $type; my @bugs = split(' ', $bugParams); my @ids = (); foreach my $bug (@bugs) { if($sayAlways || $self->needToFetchBug($event->{'target'}, $event->{'time'}, $bug)) { push @ids, $bug; $self->{'bugsHistory'}->{$event->{'target'}}->{$bug} = $event->{'time'} if $bug =~ m/^\d+$/os; } } return unless @ids; if ($subtype eq 'bug') { # Code taken from Bugzilla's xml.cgi $uri = "$self->{'bugsURI'}show_bug.cgi?ctype=xml&excludefield=long_desc&excludefield=attachmentdata&excludefield=cc".join('', map { $_ = "&id=" . $_ } @ids); $type = 'xml'; } elsif ($subtype eq 'dwim') { # XXX should escape query string my $DWIMdefaultQuery = $self->{'bugsDWIMQueryDefault'}; if (exists $self->{'bugsDWIMQueryChannelDefault'}->{$event->{'channel'}}) { $DWIMdefaultQuery = $self->{'bugsDWIMQueryChannelDefault'}->{$event->{'channel'}}; } $uri = "$self->{'bugsURI'}buglist.cgi?format=rdf&$DWIMdefaultQuery".join(',',@ids); $subtype = 'bugs'; $type = 'buglist'; } else { $uri = "$self->{'bugsURI'}buglist.cgi?format=rdf&bug_id=".join(',',@ids); $type = 'buglist'; } $self->getURI($event, $uri, $type, $subtype, $skipURI, $skipZaroo); } sub GotURI { my $self = shift; my ($event, $uri, $output, $type, $subtype, $skipURI, $skipZaroo) = @_; my @bugs; # Bugzilla really needs a LIMIT option my $maxRes; if ($event->{'channel'}) { $maxRes = 5; } else { $maxRes = 20; } my $truncated = 0; if ($type eq 'buglist') { # We asked for rdf, but old versions won't know how to do that # So lets do some simple sniffing, until mozbot gives us a way # to find out the server's returned mime type my $format; if ($output =~ /^<\?xml /) { $type = 'rdf'; } else { $type = 'html'; } } my $lots; my $bugCount; if ($type eq 'html') { my $lots; my @qp; # magicness { no warnings; # this can go _very_ wrong easily $lots = ($output !~ m//osi); # if we got truncated, then this will be missing # Instead of relying on being able to accurately count the # number of bugs (which we can't do if there are more than # 199), use the number that bugzilla tells us. if ($output =~ /(One|\d+) bugs? found/o) { $bugCount = $1; if ($bugCount eq "One") { $bugCount = 1; } } $output =~ s/<\/TABLE><\/TH>//gosi; (undef, $output) = split(/Summary<\/A><\/TH>/osi, $output); ($output, undef) = split(/<\/TABLE>/osi, $output); $output =~ s/[\n\r]//gosi; @qp = split(m/
/osi, $output); } if (scalar(@qp) == 0) { $bugCount = 0; } if (!$lots && $subtype eq 'bugs') { if (scalar(@qp) > $maxRes) { $truncated = 1; @qp = @qp[0..$maxRes-1]; } foreach (@qp) { if ($_) { # more magic if (my @d = m|\1 (.*?)(.*?)(.*?)(.*?)(.*?)(.*?)(.*)|osi) { # bugid severity priority platform owner status resolution subject my %bug; ($bug{'id'}, $bug{'severity'}, $bug{'priority'}, $bug{'platform'}, $bug{'owner'}, $bug{'status'}, $bug{'resolution'}, $bug{'summary'}) = @d; push (@bugs, \%bug); } } } } } elsif ($type eq 'xml') { # We came from xml.cgi my $parser = XML::LibXML->new(); my $tree = $parser->parse_string($output); my $root = $tree->getDocumentElement; my @xml_bugs = $root->getElementsByTagName('bug'); $bugCount = scalar(@xml_bugs); if (scalar(@xml_bugs) > $maxRes) { $truncated = 1; @xml_bugs = @xml_bugs[0..$maxRes-1]; } # OK, xml.cgi uses different names to the query stuff # Take a deep breath, and use a mapping for the fields we # care about my %fieldMap = ( 'bug_id' => 'id', 'bug_severity' => 'severity', 'priority' => 'priority', 'target_milestone' => 'target_milestone', 'assigned_to' => 'owner', 'bug_status' => 'status', 'resolution' => 'resolution', 'short_desc' => 'summary' ); foreach my $xml_bug(@xml_bugs) { my %bug = {}; my $error = $xml_bug->getAttribute('error'); if (!defined $error) { foreach my $field (keys %fieldMap) { my @arr = $xml_bug->getElementsByTagName($field); if (@arr) { my $firstChild = $arr[0]->getFirstChild(); if (defined $firstChild) { $bug{$fieldMap{$field}} = $firstChild->getData(); } } } } else { my @arr = $xml_bug->getElementsByTagName('bug_id'); $bug{'id'} = $arr[0]->getFirstChild->getData(); $bug{'error'} = $error; } push @bugs, \%bug; } } elsif ($type eq 'rdf') { my $parser = XML::LibXML->new(); my $tree = $parser->parse_string($output); my $root = $tree->getDocumentElement; my @rdf_bugs = $root->getElementsByTagName('bz:bug'); $bugCount = scalar(@rdf_bugs); if (scalar(@rdf_bugs) > $maxRes) { $truncated = 1; @rdf_bugs = @rdf_bugs[0..$maxRes-1]; } foreach my $rdf_bug (@rdf_bugs) { my %bug = {}; my @children = $rdf_bug->getChildnodes(); foreach my $child (@children) { next if ($child->getLocalName() eq 'text'); my $field = $child->getLocalName(); if ($child->getFirstChild()) { my $val = $child->getFirstChild->getData(); $bug{$field} = $val; } } push @bugs, \%bug; } } else { return $self->SUPER::GotURI(@_); } # construct the response's preamble my $preamble; if ($bugCount == 0 && !$skipZaroo) { $preamble = 'Zarro boogs found.'; } else { my $bugCountStr; if ($bugCount) { $bugCountStr = "$bugCount bug" . ($bugCount == 1 ? '' : 's') . " found"; } if ($subtype eq 'total') { $self->say($event, $bugCountStr); return; } if ($lots) { $preamble = $bugCountStr ? "$bugCountStr, which is too many for me to handle without running out of memory." : 'Way too many bugs found. I gave up so as to not run out of memory.'; $preamble .= "$bugCountStr Try to narrow your search or something!"; $subtype = 'lots'; } elsif ($subtype ne 'bug' && $bugCount > 1) { $preamble = $bugCountStr; if ($truncated) { if ($event->{'channel'}) { $preamble .= '. Five shown, please message me for more.'; } else { $preamble .= '. Will only show 20 results, please use the Bugzilla query form if you want more.'; } } } } my $prefix; if ( !$event->{'from'} || grep {$_ eq $event->{'from'}} @{$self->{'skipPrefixFor'}} ) { # they don't want to have the report prefixed with their name $prefix = ''; } else { $prefix = "$event->{'from'}: "; } if ($preamble) { $self->say($event, "$prefix$preamble"); } my $bug_link = $skipURI ? "" : "$self->{'bugsURI'}show_bug.cgi?id="; # now send out the output foreach my $bug (@bugs) { if (!defined $bug->{'error'}) { # Bugzilla doesn't give the TM by default, and we can't # change this without using cookies, which aren't supported # by the mozbot API. Later versions allow us to use a query param # but we can't detect that that was accepted, which would break # the HTML parsing # xml.cgi gives us everything, so we can print this if we got # results from there # Maybe the list of columns to display could be a var, one day, after # installations from source before Dec 2001 are no longer supported, # or we can pass cookies $self->say($event, $prefix . "Bug $bug_link$bug->{'id'} " . substr($bug->{'severity'} || $bug->{'bug_severity'}, 0, 3) . ", " . $bug->{'priority'} . ", " . ($bug->{'target_milestone'} ? "$bug->{'target_milestone'}, " : "") . ($bug->{'owner'} || $bug->{'assigned_to'}) . ", " . substr($bug->{'status'} || $bug->{'bug_status'}, 0, 4) . ($bug->{'resolution'} ? " " . $bug->{'resolution'} : "") . ", " . substr($bug->{'summary'} || $bug->{'short_desc'} || $bug->{'short_short_desc'}, 0, 100)); } elsif ($bug->{'error'} eq 'NotFound') { unless($skipZaroo) { $self->say($event, $prefix . "Bug $bug->{'id'} was not found."); } } elsif ($bug->{'error'} eq 'NotPermitted') { $self->say($event, $prefix . "Bug $bug_link$bug->{'id'} is not accessible"); } else { unless($skipZaroo) { $self->say($prefix . "Error accessing bug $bug->{'id'}: $bug->{'error'}"); } } } } sub CheckForBugMail { my $self = shift; my ($event) = @_; my ($bug_log, $bug_file) = $self->GetBugLog(); my @log_lines; if (defined $bug_log) { # We need LOCK_EX because we're going to truncate it. flock($bug_log, LOCK_EX); @log_lines = $bug_log->getlines(); $bug_log->truncate(0) or ($self->debug("Failed to truncate $bug_file: $!") && return); flock($bug_log, LOCK_UN); $bug_log->close() or $self->debug("Failed to close $bug_file: $!"); $self->debug("Read " . scalar(@log_lines) . " bugmail log lines.") if @log_lines; } else { # We will have already output a more detailed error from GetBugLog. $self->debug("CheckForBugMail Failed: Couldn't read bugmail log."); return; } # Hash to keep track of which channels we've mentioned which bug details # in, so we don't spew the same bug details over and over. my %said_bug; foreach my $line (@log_lines) { chomp($line); #$self->debug("Reading log line: $line"); my $sep = FIELD_SEPARATOR; $line =~ /^(.+)$sep(.+)$sep(.+)$sep(.+)$sep(.+)$sep(.*)$sep(.*)$sep(.+)$/; my ($bug_id, $product, $component, $who, $field, $old, $new, $message) = ($1, $2, $3, $4, $5, $6, $7, $8); # Skip this line if we never report anything for this field. next if !grep($_ eq $field, @{$self->{'reportFields'}}); my @prod_mute_fields = split(/\s*,\s*/, $self->{'productMuteFields'}->{$product}); my @chan_list; # Don't report to these channels if this product is muted for this field. push (@chan_list, $self->CreateChannelList($product, $component)) unless grep($_ eq $field, @prod_mute_fields); if ($field eq 'Product') { my @old_mute_fields = split(/\s*,\s*/, $self->{'productMuteFields'}->{$old}); push(@chan_list, $self->CreateChannelList($old, $component)) unless grep($_ eq $field, @old_mute_fields); } elsif ($field eq 'Component') { my @comp_mute_fields = @prod_mute_fields; push(@comp_mute_fields, ($self->{'productMuteFields'}->{$product. $sep . $component})); # Don't report it if the product is muted for this field, or if # this specific component is muted for this field. push(@chan_list, $self->CreateChannelList($product, $old)) unless grep($_ eq $field, @comp_mute_fields); } # Enable Mozbot to report both product and component of new bugs. if (grep(lc($_) eq 'all', @{$self->{'reportComponent'}}) || grep(lc($_) eq lc($product), @{$self->{'reportComponent'}}) || grep(lc($_) eq lc($product.$sep.$component), @{$self->{'reportComponent'}})) { $message =~ s/^New $product bug/New $product - $component bug/i; } unless ($self->ignoringMailProducedBy($who)) { # Keep track of which channels we've told already, to avoid # duplicate messages. my %said_to; foreach my $channel (@chan_list) { my @chan_mute_fields = split(/\s*,\s*/, $self->{'channelMuteFields'}->{$channel}); # Don't say it if we've said it before, or if this # field is muted in this channel. unless ( $said_to{$channel} || grep($_ eq $field, @chan_mute_fields) ) { # We can't use "local" here, or the target doesn't show # up properly in the GotURI after FetchBug. $event->{'target'} = $channel; $self->say($event, $message); my $bugids = ""; # Special case for "duplicate of messages" if ($message =~ /DUPLICATE of bug (\d+)/) { my $dup_id = $1; $bugids = $dup_id unless $said_bug{$channel . $dup_id}; $said_bug{$channel . $dup_id} = 1; } # Fetch bugs mentioned for dependent field changes if ($field eq 'OtherBugsDependingOnThis' || $field eq 'BugsThisDependsOn') { foreach my $id (split(/,/, $old . $new)) { $bugids = $id . " " . $bugids unless $said_bug{$channel . $id}; $said_bug{$channel . $id} = 1; } } if (! $said_bug{$channel . $bug_id}) { $bugids = $bug_id . " " . $bugids; } if ($bugids ne '') { if ($self->{'reportBugDetails'}) { $self->FetchBug($event, $bugids, 'bug'); } } $said_to{$channel} = 1; $said_bug{$channel . $bug_id} = 1; } # unless $said_to } # foreach @chan_list } # unless ignoringMailProducedBy } # foreach @log_lines } # A helper for CheckForBugMail. sub CreateChannelList { my $self = shift; my ($product, $component) = @_; my $chan_list = ""; ($chan_list .= $self->{'productReportChannels'}->{$product}) if $self->{'productReportChannels'}->{$product}; my $prodcomp = $product . FIELD_SEPARATOR . $component; ($chan_list .= ',' . $self->{'productReportChannels'}->{$prodcomp}) if $self->{'productReportChannels'}->{$prodcomp}; return (split /\s*,\s*/, $chan_list); } # Creates the BUGMAIL_LOG file if it doesn't exist, and returns # an open IO::File for it, and also the filename of that file. sub GetBugLog { my $self = shift; my $file_name = dirname($0) . '/' . BUGMAIL_LOG; # And we generally trust $bug_log to be an OK path, so untaint it now. $file_name =~ /^(.*)$/; $file_name = $1; my $file = new IO::File($file_name, O_RDWR | O_CREAT, 0660) or $self->debug("Could not open/create $file_name for reading" . " incoming bugmail: $!"); return ($file, $file_name); } sub ignoringMailProducedBy { my $self = shift; my ($who) = @_; return grep($_ eq $who, @{$self->{'mailIgnore'}}) ? 1 : 0; } sub ignoringCommentsTo { my $self = shift; my ($who) = @_; foreach (@{$self->{'ignoreCommentsTo'}}) { next unless $_; # Ignore blanks, happens when the array is empty (?) return 1 if $who =~ m/^(?:.*[]\s,.;:\\\/=?!()<>{}[-])?\Q$_\E(?:[]\s,.;:\\\/=?!()<>{}[-].*)?$/is; } return 0; } sub ignoringCommentsFrom { my $self = shift; my ($who) = @_; foreach (@{$self->{'ignoreCommentsFrom'}}) { return 1 if $_ eq $who; } return 0; } sub needToFetchBug { my ($self, $target, $time, $bug) = @_; my $last = 0; if (defined($self->{'bugsHistory'}->{$target}->{$bug})) { $last = $self->{'bugsHistory'}->{$target}->{$bug}; } if (($time-$last) > $self->{'backoffTime'}) { return 1; } return 0; }