mirror of
https://github.com/znc/znc.git
synced 2026-03-28 17:42:41 +01:00
585 lines
12 KiB
Perl
585 lines
12 KiB
Perl
#
|
|
# Copyright (C) 2004-2011 See the AUTHORS file for details.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify it
|
|
# under the terms of the GNU General Public License version 2 as published
|
|
# by the Free Software Foundation.
|
|
#
|
|
|
|
use 5.010;
|
|
use strict;
|
|
use warnings;
|
|
use ZNC;
|
|
use IO::File;
|
|
use feature 'switch', 'say';
|
|
|
|
package ZNC::Core;
|
|
|
|
my $uuidtype;
|
|
my $uuidgen;
|
|
our %pmods;
|
|
my %modrefcount;
|
|
|
|
sub Init {
|
|
if (eval { require Data::UUID }) {
|
|
$uuidtype = 'Data::UUID';
|
|
$uuidgen = new Data::UUID;
|
|
} elsif (eval { require UUID }) {
|
|
$uuidtype = 'UUID';
|
|
} else {
|
|
$uuidtype = 'int';
|
|
$uuidgen = 0;
|
|
}
|
|
}
|
|
|
|
sub CreateUUID {
|
|
my $res;
|
|
given ($uuidtype) {
|
|
when ('Data::UUID') {
|
|
$res = $uuidgen->create_str;
|
|
}
|
|
when ('UUID') {
|
|
my ($uuid, $str);
|
|
UUID::generate($uuid);
|
|
UUID::unparse($uuid, $res);
|
|
}
|
|
when ('int') {
|
|
$uuidgen++;
|
|
$res = "$uuidgen";
|
|
}
|
|
}
|
|
say "Created new UUID for modperl with '$uuidtype': $res";
|
|
return $res;
|
|
}
|
|
|
|
sub unloadByIDUser {
|
|
my ($id, $user) = @_;
|
|
my $modpath = $pmods{$id}{_cmod}->GetModPath;
|
|
my $modname = $pmods{$id}{_cmod}->GetModName;
|
|
$pmods{$id}->OnShutdown;
|
|
$user->GetModules->removeModule($pmods{$id}{_cmod});
|
|
delete $pmods{$id}{_cmod};# Just for the case
|
|
delete $pmods{$id}{_nv};
|
|
delete $pmods{$id}{_ptimers};
|
|
delete $pmods{$id}{_sockets};
|
|
delete $pmods{$id};
|
|
unless (--$modrefcount{$modname}) {
|
|
say "Unloading $modpath from perl";
|
|
ZNC::_CleanupStash($modname);
|
|
delete $INC{$modpath};
|
|
}
|
|
}
|
|
|
|
sub UnloadModule {
|
|
my ($cmod) = @_;
|
|
unloadByIDUser($cmod->GetPerlID, $cmod->GetUser);
|
|
}
|
|
|
|
sub UnloadAll {
|
|
while (my ($id, $pmod) = each %pmods) {
|
|
unloadByIDUser($id, $pmod->{_cmod}->GetUser);
|
|
}
|
|
}
|
|
|
|
sub IsModule {
|
|
my $path = shift;
|
|
my $modname = shift;
|
|
my $f = IO::File->new($path);
|
|
grep {/package\s+$modname\s*;/} <$f>;
|
|
}
|
|
|
|
sub LoadModule {
|
|
my ($modname, $args, $user) = @_;
|
|
$modname =~ /^\w+$/ or return ($ZNC::Perl_LoadError, "Module names can only contain letters, numbers and underscores, [$modname] is invalid.");
|
|
return ($ZNC::Perl_LoadError, "Module [$modname] already loaded.") if defined $user->GetModules->FindModule($modname);
|
|
my $modpath = ZNC::String->new;
|
|
my $datapath = ZNC::String->new;
|
|
ZNC::CModules::FindModPath("$modname.pm", $modpath, $datapath) or return ($ZNC::Perl_NotFound);
|
|
$modpath = $modpath->GetPerlStr;
|
|
return ($ZNC::Perl_LoadError, "Incorrect perl module [$modpath]") unless IsModule $modpath, $modname;
|
|
eval {
|
|
require $modpath;
|
|
};
|
|
if ($@) {
|
|
# modrefcount was 0 before this, otherwise it couldn't die.
|
|
# so can safely remove module from %INC
|
|
delete $INC{$modpath};
|
|
die $@;
|
|
}
|
|
$modrefcount{$modname}++;
|
|
my $id = CreateUUID;
|
|
$datapath = $datapath->GetPerlStr;
|
|
$datapath =~ s/\.pm$//;
|
|
my $cmod = ZNC::CPerlModule->new($user, $modname, $datapath, $id);
|
|
my %nv;
|
|
tie %nv, 'ZNC::ModuleNV', $cmod;
|
|
my $pmod = bless {
|
|
_cmod=>$cmod,
|
|
_nv=>\%nv
|
|
}, $modname;
|
|
$cmod->SetDescription($pmod->description);
|
|
$cmod->SetArgs($args);
|
|
$cmod->SetModPath($modpath);
|
|
$pmods{$id} = $pmod;
|
|
$user->GetModules->push_back($cmod);
|
|
my $x = '';
|
|
my $loaded = 0;
|
|
eval {
|
|
$loaded = $pmod->OnLoad($args, $x);
|
|
};
|
|
if ($@) {
|
|
$x .= ' ' if '' ne $x;
|
|
$x .= $@;
|
|
}
|
|
if (!$loaded) {
|
|
unloadByIDUser($id, $user);
|
|
if ($x) {
|
|
return ($ZNC::Perl_LoadError, "Module [$modname] aborted: $x");
|
|
}
|
|
return ($ZNC::Perl_LoadError, "Module [$modname] aborted.");
|
|
}
|
|
if ($x) {
|
|
return ($ZNC::Perl_Loaded, "[$x] [$modpath]");
|
|
}
|
|
return ($ZNC::Perl_Loaded, "[$modpath]")
|
|
}
|
|
|
|
sub GetModInfo {
|
|
my ($modname) = @_;
|
|
$modname =~ /^\w+$/ or return ($ZNC::Perl_LoadError, "Module names can only contain letters, numbers and underscores, [$modname] is invalid.");
|
|
my $modpath = ZNC::String->new;
|
|
my $datapath = ZNC::String->new;
|
|
ZNC::CModules::FindModPath("$modname.pm", $modpath, $datapath) or return ($ZNC::Perl_NotFound, "Unable to find module [$modname]");
|
|
$modpath = $modpath->GetPerlStr;
|
|
return ($ZNC::Perl_LoadError, "Incorrect perl module.") unless IsModule $modpath, $modname;
|
|
require $modpath;
|
|
my $pmod = bless {}, $modname;
|
|
return ($ZNC::Perl_Loaded, $modpath, $pmod->description, $pmod->wiki_page)
|
|
}
|
|
|
|
sub ModInfoByPath {
|
|
my ($modpath, $modname) = @_;
|
|
die "Incorrect perl module." unless IsModule $modpath, $modname;
|
|
require $modpath;
|
|
my $pmod = bless {}, $modname;
|
|
return ($pmod->description, $pmod->wiki_page)
|
|
}
|
|
|
|
sub CallModFunc {
|
|
my $id = shift;
|
|
my $func = shift;
|
|
my $default = shift;
|
|
my @arg = @_;
|
|
my $res = $pmods{$id}->$func(@arg);
|
|
# print "Returned from $func(@_): $res, (@arg)\n";
|
|
unless (defined $res) {
|
|
$res = $default if defined $default;
|
|
}
|
|
($res, @arg)
|
|
}
|
|
|
|
sub CallTimer {
|
|
my $modid = shift;
|
|
my $timerid = shift;
|
|
$pmods{$modid}->_CallTimer($timerid)
|
|
}
|
|
|
|
sub CallSocket {
|
|
my $modid = shift;
|
|
$pmods{$modid}->_CallSocket(@_)
|
|
}
|
|
|
|
sub RemoveTimer {
|
|
my $modid = shift;
|
|
my $timerid = shift;
|
|
$pmods{$modid}->_RemoveTimer($timerid)
|
|
}
|
|
|
|
sub RemoveSocket {
|
|
my $modid = shift;
|
|
my $sockid = shift;
|
|
$pmods{$modid}->_RemoveSocket($sockid)
|
|
}
|
|
|
|
package ZNC::ModuleNV;
|
|
|
|
sub TIEHASH {
|
|
my $name = shift;
|
|
my $cmod = shift;
|
|
bless {cmod=>$cmod, last=>-1}, $name
|
|
}
|
|
|
|
sub FETCH {
|
|
my $self = shift;
|
|
my $key = shift;
|
|
return $self->{cmod}->GetNV($key) if $self->{cmod}->ExistsNV($key);
|
|
return undef
|
|
}
|
|
|
|
sub STORE {
|
|
my $self = shift;
|
|
my $key = shift;
|
|
my $value = shift;
|
|
$self->{cmod}->SetNV($key, $value);
|
|
}
|
|
|
|
sub DELETE {
|
|
my $self = shift;
|
|
my $key = shift;
|
|
$self->{cmod}->DelNV($key);
|
|
}
|
|
|
|
sub CLEAR {
|
|
my $self = shift;
|
|
$self->{cmod}->ClearNV;
|
|
}
|
|
|
|
sub EXISTS {
|
|
my $self = shift;
|
|
my $key = shift;
|
|
$self->{cmod}->ExistsNV($key)
|
|
}
|
|
|
|
sub FIRSTKEY {
|
|
my $self = shift;
|
|
my @keys = $self->{cmod}->GetNVKeys;
|
|
$self->{last} = 0;
|
|
return $keys[0];
|
|
return undef;
|
|
}
|
|
|
|
sub NEXTKEY {
|
|
my $self = shift;
|
|
my $last = shift;
|
|
my @keys = $self->{cmod}->GetNVKeys;
|
|
if ($#keys < $self->{last}) {
|
|
$self->{last} = -1;
|
|
return undef
|
|
}
|
|
# Probably caller called delete on last key?
|
|
if ($last eq $keys[$self->{last}]) {
|
|
$self->{last}++
|
|
}
|
|
if ($#keys < $self->{last}) {
|
|
$self->{last} = -1;
|
|
return undef
|
|
}
|
|
return $keys[$self->{last}]
|
|
}
|
|
|
|
sub SCALAR {
|
|
my $self = shift;
|
|
my @keys = $self->{cmod}->GetNVKeys;
|
|
return $#keys + 1
|
|
}
|
|
|
|
package ZNC::Module;
|
|
|
|
sub description {
|
|
"< Placeholder for a description >"
|
|
}
|
|
|
|
sub wiki_page {
|
|
''
|
|
}
|
|
|
|
# Default implementations for module hooks. They can be overriden in derived modules.
|
|
sub OnLoad {1}
|
|
sub OnBoot {}
|
|
sub OnShutdown {}
|
|
sub WebRequiresLogin {}
|
|
sub WebRequiresAdmin {}
|
|
sub GetWebMenuTitle {}
|
|
sub OnWebPreRequest {}
|
|
sub OnWebRequest {}
|
|
sub GetSubPages {}
|
|
sub _GetSubPages { my $self = shift; $self->GetSubPages }
|
|
sub OnPreRehash {}
|
|
sub OnPostRehash {}
|
|
sub OnIRCDisconnected {}
|
|
sub OnIRCConnected {}
|
|
sub OnIRCConnecting {}
|
|
sub OnIRCConnectionError {}
|
|
sub OnIRCRegistration {}
|
|
sub OnBroadcast {}
|
|
sub OnChanPermission {}
|
|
sub OnOp {}
|
|
sub OnDeop {}
|
|
sub OnVoice {}
|
|
sub OnDevoice {}
|
|
sub OnMode {}
|
|
sub OnRawMode {}
|
|
sub OnRaw {}
|
|
sub OnStatusCommand {}
|
|
sub OnModCommand {}
|
|
sub OnModNotice {}
|
|
sub OnModCTCP {}
|
|
sub OnQuit {}
|
|
sub OnNick {}
|
|
sub OnKick {}
|
|
sub OnJoin {}
|
|
sub OnPart {}
|
|
sub OnChanBufferStarting {}
|
|
sub OnChanBufferEnding {}
|
|
sub OnChanBufferPlayLine {}
|
|
sub OnPrivBufferPlayLine {}
|
|
sub OnClientLogin {}
|
|
sub OnClientDisconnect {}
|
|
sub OnUserRaw {}
|
|
sub OnUserCTCPReply {}
|
|
sub OnUserCTCP {}
|
|
sub OnUserAction {}
|
|
sub OnUserMsg {}
|
|
sub OnUserNotice {}
|
|
sub OnUserJoin {}
|
|
sub OnUserPart {}
|
|
sub OnUserTopic {}
|
|
sub OnUserTopicRequest {}
|
|
sub OnCTCPReply {}
|
|
sub OnPrivCTCP {}
|
|
sub OnChanCTCP {}
|
|
sub OnPrivAction {}
|
|
sub OnChanAction {}
|
|
sub OnPrivMsg {}
|
|
sub OnChanMsg {}
|
|
sub OnPrivNotice {}
|
|
sub OnChanNotice {}
|
|
sub OnTopic {}
|
|
sub OnServerCapAvailable {}
|
|
sub OnServerCapResult {}
|
|
sub OnTimerAutoJoin {}
|
|
sub OnEmbeddedWebRequest {}
|
|
|
|
|
|
# Functions of CModule will be usable from perl modules.
|
|
our $AUTOLOAD;
|
|
|
|
sub AUTOLOAD {
|
|
my $name = $AUTOLOAD;
|
|
$name =~ s/^.*:://; # Strip fully-qualified portion.
|
|
my $sub = sub {
|
|
my $self = shift;
|
|
$self->{_cmod}->$name(@_)
|
|
};
|
|
no strict 'refs';
|
|
*{$AUTOLOAD} = $sub;
|
|
use strict 'refs';
|
|
goto &{$sub};
|
|
}
|
|
|
|
sub DESTROY {}
|
|
|
|
sub BeginNV {
|
|
die "Don't use BeginNV from perl modules, use GetNVKeys or NV instead!";
|
|
}
|
|
sub EndNV {
|
|
die "Don't use EndNV from perl modules, use GetNVKeys or NV instead!";
|
|
}
|
|
sub FindNV {
|
|
die "Don't use FindNV from perl modules, use GetNVKeys/ExistsNV or NV instead!";
|
|
}
|
|
|
|
sub NV {
|
|
my $self = shift;
|
|
$self->{_nv}
|
|
}
|
|
|
|
sub CreateTimer {
|
|
my $self = shift;
|
|
my $id = ZNC::Core::CreateUUID;
|
|
my %a = @_;
|
|
my $ctimer = ZNC::CreatePerlTimer(
|
|
$self->{_cmod},
|
|
$a{interval}//10,
|
|
$a{cycles}//1,
|
|
"perl-timer-$id",
|
|
$a{description}//'Just Another Perl Timer',
|
|
$id);
|
|
my $ptimer = {
|
|
_ctimer=>$ctimer,
|
|
_modid=>$self->GetPerlID
|
|
};
|
|
$self->{_ptimers}{$id} = $ptimer;
|
|
if (ref($a{task}) eq 'CODE') {
|
|
bless $ptimer, 'ZNC::Timer';
|
|
$ptimer->{job} = $a{task};
|
|
$ptimer->{context} = $a{context};
|
|
} else {
|
|
bless $ptimer, $a{task};
|
|
}
|
|
$ptimer;
|
|
}
|
|
|
|
sub _CallTimer {
|
|
my $self = shift;
|
|
my $id = shift;
|
|
my $t = $self->{_ptimers}{$id};
|
|
$t->RunJob;
|
|
}
|
|
|
|
sub _RemoveTimer {
|
|
my $self = shift;
|
|
my $id = shift;
|
|
say "Removing perl timer $id";
|
|
$self->{_ptimers}{$id}->OnShutdown;
|
|
delete $self->{_ptimers}{$id}
|
|
}
|
|
|
|
sub CreateSocket {
|
|
my $self = shift;
|
|
my $class = shift;
|
|
my $id = ZNC::Core::CreateUUID;
|
|
my $csock = ZNC::CreatePerlSocket($self->{_cmod}, $id);
|
|
my $psock = bless {
|
|
_csock=>$csock,
|
|
_modid=>$self->GetPerlID
|
|
}, $class;
|
|
$self->{_sockets}{$id} = $psock;
|
|
$psock->Init(@_);
|
|
$psock;
|
|
}
|
|
|
|
sub _CallSocket {
|
|
my $self = shift;
|
|
my $id = shift;
|
|
my $func = shift;
|
|
$self->{_sockets}{$id}->$func(@_)
|
|
}
|
|
|
|
sub _RemoveSocket {
|
|
my $self = shift;
|
|
my $id = shift;
|
|
say "Removing perl socket $id";
|
|
$self->{_sockets}{$id}->OnShutdown;
|
|
delete $self->{_sockets}{$id}
|
|
}
|
|
|
|
package ZNC::Timer;
|
|
|
|
sub GetModule {
|
|
my $self = shift;
|
|
$ZNC::Core::pmods{$self->{_modid}};
|
|
}
|
|
|
|
sub RunJob {
|
|
my $self = shift;
|
|
if (ref($self->{job}) eq 'CODE') {
|
|
&{$self->{job}}($self->GetModule, context=>$self->{context}, timer=>$self->{_ctimer});
|
|
}
|
|
}
|
|
|
|
sub OnShutdown {}
|
|
|
|
our $AUTOLOAD;
|
|
|
|
sub AUTOLOAD {
|
|
my $name = $AUTOLOAD;
|
|
$name =~ s/^.*:://; # Strip fully-qualified portion.
|
|
my $sub = sub {
|
|
my $self = shift;
|
|
$self->{_ctimer}->$name(@_)
|
|
};
|
|
no strict 'refs';
|
|
*{$AUTOLOAD} = $sub;
|
|
use strict 'refs';
|
|
goto &{$sub};
|
|
}
|
|
|
|
sub DESTROY {}
|
|
|
|
|
|
package ZNC::Socket;
|
|
|
|
sub GetModule {
|
|
my $self = shift;
|
|
$ZNC::Core::pmods{$self->{_modid}};
|
|
}
|
|
|
|
sub Init {}
|
|
sub OnConnected {}
|
|
sub OnDisconnected {}
|
|
sub OnTimeout {}
|
|
sub OnConnectionRefused {}
|
|
sub OnReadData {}
|
|
sub OnReadLine {}
|
|
sub OnAccepted {}
|
|
sub OnShutdown {}
|
|
|
|
sub _Accepted {
|
|
my $self = shift;
|
|
my $psock = $self->OnAccepted(@_);
|
|
return $psock->{_csock} if defined $psock;
|
|
return undef;
|
|
}
|
|
|
|
our $AUTOLOAD;
|
|
|
|
sub AUTOLOAD {
|
|
my $name = $AUTOLOAD;
|
|
$name =~ s/^.*:://; # Strip fully-qualified portion.
|
|
my $sub = sub {
|
|
my $self = shift;
|
|
$self->{_csock}->$name(@_)
|
|
};
|
|
no strict 'refs';
|
|
*{$AUTOLOAD} = $sub;
|
|
use strict 'refs';
|
|
goto &{$sub};
|
|
}
|
|
|
|
sub DESTROY {}
|
|
|
|
sub Connect {
|
|
my $self = shift;
|
|
my $host = shift;
|
|
my $port = shift;
|
|
my %arg = @_;
|
|
$self->GetModule->GetManager->Connect(
|
|
$host,
|
|
$port,
|
|
"perl-socket-".$self->GetPerlID,
|
|
$arg{timeout}//60,
|
|
$arg{ssl}//0,
|
|
$arg{bindhost}//'',
|
|
$self->{_csock}
|
|
);
|
|
}
|
|
|
|
sub Listen {
|
|
my $self = shift;
|
|
my %arg = @_;
|
|
my $addrtype = $ZNC::ADDR_ALL;
|
|
if (defined $arg{addrtype}) {
|
|
given ($arg{addrtype}) {
|
|
when (/^ipv4$/i) { $addrtype = $ZNC::ADDR_IPV4ONLY }
|
|
when (/^ipv6$/i) { $addrtype = $ZNC::ADDR_IPV6ONLY }
|
|
when (/^all$/i) { }
|
|
default { die "Specified addrtype [$arg{addrtype}] isn't supported" }
|
|
}
|
|
}
|
|
if (defined $arg{port}) {
|
|
return $arg{port} if $self->GetModule->GetManager->ListenHost(
|
|
$arg{port},
|
|
"perl-socket-".$self->GetPerlID,
|
|
$arg{bindhost}//'',
|
|
$arg{ssl}//0,
|
|
$arg{maxconns}//ZNC::_GetSOMAXCONN,
|
|
$self->{_csock},
|
|
$arg{timeout}//0,
|
|
$addrtype
|
|
);
|
|
return 0;
|
|
}
|
|
$self->GetModule->GetManager->ListenRand(
|
|
"perl-socket-".$self->GetPerlID,
|
|
$arg{bindhost}//'',
|
|
$arg{ssl}//0,
|
|
$arg{maxconns}//ZNC::_GetSOMAXCONN,
|
|
$self->{_csock},
|
|
$arg{timeout}//0,
|
|
$addrtype
|
|
);
|
|
}
|
|
|
|
1
|