| # <@LICENSE> |
| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to you under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at: |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # </@LICENSE> |
| |
| =head1 NAME |
| |
| Mail::SpamAssassin::PluginHandler - SpamAssassin plugin handler |
| |
| =cut |
| |
| package Mail::SpamAssassin::PluginHandler; |
| |
| use Mail::SpamAssassin; |
| use Mail::SpamAssassin::Plugin; |
| use Mail::SpamAssassin::Util; |
| use Mail::SpamAssassin::Logger; |
| |
| use strict; |
| use warnings; |
| # use bytes; |
| use re 'taint'; |
| use File::Spec; |
| |
| our @ISA = qw(); |
| |
| #Removed $VERSION per BUG 6422 |
| #$VERSION = 'bogus'; # avoid CPAN.pm picking up version strings later |
| |
| # Normally, the list of active plugins that should be called for a given hook |
| # method name is compiled and cached at runtime. This means that later calls |
| # will not have to traverse the entire plugin list more than once, since the |
| # list of plugins that implement that hook is already cached. |
| # |
| # However, some hooks should not receive this treatment. One of these is |
| # parse_config, which may be compiled before all config files have been read; |
| # if a plugin is loaded from a config file after this has been compiled, it |
| # will not get callbacks. |
| # |
| # Any other such hooks that may be compiled at config-parse-time should be |
| # listed here. |
| |
| our @CONFIG_TIME_HOOKS = qw( parse_config ); |
| |
| ########################################################################### |
| |
| sub new { |
| my $class = shift; |
| my $main = shift; |
| $class = ref($class) || $class; |
| my $self = { |
| plugins => [ ], |
| cbs => { }, |
| main => $main |
| }; |
| bless ($self, $class); |
| $self; |
| } |
| |
| ########################################################################### |
| |
| sub load_plugin { |
| my ($self, $package, $path, $silent) = @_; |
| |
| # Strict name checking |
| if ($package !~ /^(?:\w+::){0,10}\w+$/) { |
| warn "plugin: illegal plugin name, not loading: $package\n"; |
| return; |
| } |
| $package = Mail::SpamAssassin::Util::untaint_var($package); |
| |
| # Bug 7728 |
| if ($package eq 'Mail::SpamAssassin::Plugin::HashCash') { |
| warn "plugin: $package is deprecated, remove loadplugin clause from your configuration\n"; |
| return; |
| } |
| |
| # Don't load the same plugin twice! |
| # Do this *before* calling ->new(), otherwise eval rules will be |
| # registered on a nonexistent object |
| foreach my $old_plugin (@{$self->{plugins}}) { |
| if (ref($old_plugin) eq $package) { |
| dbg("plugin: did not register $package, already registered"); |
| return; |
| } |
| } |
| |
| my $ret; |
| if ($path) { |
| if ($path !~ /^\S+\.pm/i) { |
| warn "plugin: illegal plugin filename, not loading: $path"; |
| return; |
| } |
| |
| $path = $self->{main}->{conf}->{parser}->fix_path_relative_to_current_file($path); |
| |
| # bug 3717: |
| # At least Perl 5.8.0 seems to confuse $cwd internally at some point -- we |
| # need to use an absolute path here else we get a "File not found" error. |
| $path = Mail::SpamAssassin::Util::untaint_file_path( |
| File::Spec->rel2abs($path) |
| ); |
| |
| # if (exists $INC{$path}) { |
| # dbg("plugin: not loading $package from $path, already loaded"); |
| # return; |
| # } |
| |
| dbg("plugin: loading $package from $path"); |
| |
| # use require instead of "do", so we get built-in $INC{filename} |
| # smarts |
| $ret = eval { require $path; }; |
| } |
| else { |
| dbg("plugin: loading $package from \@INC"); |
| $ret = eval qq{ require $package; }; |
| $path = "(from \@INC)"; |
| } |
| |
| if (!$ret) { |
| if ($silent) { |
| if ($@) { dbg("plugin: failed to parse tryplugin $path: $@\n"); } |
| elsif ($!) { dbg("plugin: failed to load tryplugin $path: $!\n"); } |
| } |
| else { |
| if ($@) { warn "plugin: failed to parse plugin $path: $@\n"; } |
| elsif ($!) { warn "plugin: failed to load plugin $path: $!\n"; } |
| } |
| return; # failure! no point in continuing here |
| } |
| |
| my $plugin = eval $package.q{->new ($self->{main}); }; |
| |
| if ($@ || !$plugin) { |
| warn "plugin: failed to create instance of plugin $package: $@\n"; |
| } |
| |
| if ($plugin) { |
| $self->{main}->{plugins}->register_plugin ($plugin); |
| $self->{main}->{conf}->load_plugin_succeeded ($plugin, $package, $path); |
| } |
| } |
| |
| sub register_plugin { |
| my ($self, $plugin) = @_; |
| $plugin->{main} = $self->{main}; |
| push (@{$self->{plugins}}, $plugin); |
| # dbg("plugin: registered $plugin"); |
| |
| # invalidate cache entries for any configuration-time hooks, in case |
| # one has already been built; this plugin may implement that hook! |
| foreach my $subname (@CONFIG_TIME_HOOKS) { |
| delete $self->{cbs}->{$subname}; |
| } |
| } |
| |
| ########################################################################### |
| |
| sub have_callback { |
| my ($self, $subname) = @_; |
| |
| # have we set up the cache entry for this callback type? |
| if (!exists $self->{cbs}->{$subname}) { |
| # nope. run through all registered plugins and see which ones |
| # implement this type of callback. sort by priority |
| |
| my %subsbypri; |
| foreach my $plugin (@{$self->{plugins}}) { |
| my $methodref = $plugin->can ($subname); |
| if (defined $methodref) { |
| my $pri = $plugin->{method_priority}->{$subname} || 0; |
| |
| $subsbypri{$pri} ||= []; |
| push (@{$subsbypri{$pri}}, [ $plugin, $methodref ]); |
| |
| dbg("plugin: ${plugin} implements '$subname', priority $pri"); |
| } |
| } |
| |
| my @subs; |
| foreach my $pri (sort { $a <=> $b } keys %subsbypri) { |
| push @subs, @{$subsbypri{$pri}}; |
| } |
| |
| $self->{cbs}->{$subname} = \@subs; |
| } |
| |
| return scalar(@{$self->{cbs}->{$subname}}); |
| } |
| |
| sub callback { |
| my $self = shift; |
| my $subname = shift; |
| my ($ret, $overallret); |
| |
| # have we set up the cache entry for this callback type? |
| if (!exists $self->{cbs}->{$subname}) { |
| return unless $self->have_callback($subname); |
| } |
| |
| foreach my $cbpair (@{$self->{cbs}->{$subname}}) { |
| my ($plugin, $methodref) = @$cbpair; |
| |
| $plugin->{_inhibit_further_callbacks} = 0; |
| |
| eval { |
| $ret = &$methodref ($plugin, @_); |
| 1; |
| } or do { |
| my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat; |
| warn "plugin: eval failed: $eval_stat\n"; |
| }; |
| |
| if (defined $ret) { |
| # dbg("plugin: ${plugin}->${methodref} => $ret"); |
| # we are interested in defined but false results too |
| $overallret = $ret if $ret || !defined $overallret; |
| } |
| |
| if ($plugin->{_inhibit_further_callbacks}) { |
| # dbg("plugin: $plugin inhibited further callbacks"); |
| last; |
| } |
| } |
| |
| return $overallret; |
| } |
| |
| ########################################################################### |
| |
| sub get_loaded_plugins_list { |
| my ($self) = @_; |
| return @{$self->{plugins}}; |
| } |
| |
| ########################################################################### |
| |
| sub finish { |
| my $self = shift; |
| delete $self->{cbs}; |
| foreach my $plugin (@{$self->{plugins}}) { |
| $plugin->finish(); |
| delete $plugin->{main}; |
| } |
| delete $self->{plugins}; |
| delete $self->{main}; |
| } |
| |
| ########################################################################### |
| |
| 1; |