—# <@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:
#
#
# 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
strict;
use
warnings;
# use bytes;
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;