3
0
mirror of https://github.com/pragma-/pbot.git synced 2025-10-14 15:07:22 +02:00

Factoids: Restrict recursion depth for commands that modify factoids (factadd, factchange, etc)

This commit is contained in:
Pragmatic Software 2025-09-04 10:24:55 -07:00
parent 0ecdcc1bf5
commit 9cf4b18538
No known key found for this signature in database
GPG Key ID: CC916B6E3C84ECCE
5 changed files with 100 additions and 12 deletions

View File

@ -132,11 +132,10 @@ sub interpreter($self, $context) {
# debug flag to trace $context location and contents # debug flag to trace $context location and contents
if ($self->{pbot}->{registry}->get_value('general', 'debugcontext')) { if ($self->{pbot}->{registry}->get_value('general', 'debugcontext')) {
use Data::Dumper; use Data::Dumper;
$Data::Dumper::Sortkeys = sub { [sort grep { not /(?:cmdlist|arglist)/ } keys %$context] }; $Data::Dumper::Sortkeys = 1;
$Data::Dumper::Indent = 2; $Data::Dumper::Indent = 2;
$self->{pbot}->{logger}->log("Commands::interpreter\n"); $self->{pbot}->{logger}->log("Commands::interpreter\n");
$self->{pbot}->{logger}->log(Dumper $context); $self->{pbot}->{logger}->log(Dumper $context);
$Data::Dumper::Sortkeys = 1;
} }
# some convenient aliases # some convenient aliases

View File

@ -133,6 +133,10 @@ sub cmd_as_factoid($self, $context) {
sub cmd_factundo($self, $context) { sub cmd_factundo($self, $context) {
my $usage = "Usage: factundo [-l [N]] [-r N] [channel] <keyword> (-l list undo history, optionally starting from N; -r jump to revision N)"; my $usage = "Usage: factundo [-l [N]] [-r N] [channel] <keyword> (-l list undo history, optionally starting from N; -r jump to revision N)";
if ($context->{interpret_depth} > 2) {
return "factundo: recursion depth exceeded.\n";
}
my $arguments = $context->{arguments}; my $arguments = $context->{arguments};
my ($list_undos, $goto_revision); my ($list_undos, $goto_revision);
@ -196,8 +200,13 @@ sub cmd_factundo($self, $context) {
return $self->list_undo_history($undos, $list_undos); return $self->list_undo_history($undos, $list_undos);
} }
my $factoids = $self->{pbot}->{factoids}->{data}->{storage};
my $userinfo = $self->{pbot}->{users}->loggedin($channel, $context->{hostmask}); my $userinfo = $self->{pbot}->{users}->loggedin($channel, $context->{hostmask});
if (!$self->{pbot}->{capabilities}->userhas($userinfo, 'admin') && $channel =~ /^#/ && $channel ne $context->{from}) {
return "/say Switch to $channel to revert this factoid.";
}
my $factoids = $self->{pbot}->{factoids}->{data}->{storage};
if ($factoids->get_data($channel, $trigger, 'locked')) { if ($factoids->get_data($channel, $trigger, 'locked')) {
return "/say $trigger_name is locked and cannot be reverted." if not $self->{pbot}->{capabilities}->userhas($userinfo, 'admin'); return "/say $trigger_name is locked and cannot be reverted." if not $self->{pbot}->{capabilities}->userhas($userinfo, 'admin');
@ -236,6 +245,10 @@ sub cmd_factundo($self, $context) {
sub cmd_factredo($self, $context) { sub cmd_factredo($self, $context) {
my $usage = "Usage: factredo [-l [N]] [-r N] [channel] <keyword> (-l list undo history, optionally starting from N; -r jump to revision N)"; my $usage = "Usage: factredo [-l [N]] [-r N] [channel] <keyword> (-l list undo history, optionally starting from N; -r jump to revision N)";
if ($context->{interpret_depth} >2) {
return "factredo: recursion depth exceeded.\n";
}
my $arguments = $context->{arguments}; my $arguments = $context->{arguments};
@ -287,6 +300,10 @@ sub cmd_factredo($self, $context) {
return $self->list_undo_history($undos, $list_undos); return $self->list_undo_history($undos, $list_undos);
} }
if ($channel =~ /^#/ && $channel ne $context->{from}) {
return "/say Switch to $channel to revert this factoid.";
}
my $factoids = $self->{pbot}->{factoids}->{data}->{storage}; my $factoids = $self->{pbot}->{factoids}->{data}->{storage};
my $userinfo = $self->{pbot}->{users}->loggedin($channel, $context->{hostmask}); my $userinfo = $self->{pbot}->{users}->loggedin($channel, $context->{hostmask});
if ($factoids->get_data($channel, $trigger, 'locked')) { if ($factoids->get_data($channel, $trigger, 'locked')) {
@ -331,6 +348,10 @@ sub cmd_factset($self, $context) {
$context->{from}, $context->{arguments}, 'factset', usage => 'Usage: factset [channel] <factoid> [key [value]]', explicit => 1 $context->{from}, $context->{arguments}, 'factset', usage => 'Usage: factset [channel] <factoid> [key [value]]', explicit => 1
); );
if ($context->{interpret_depth} > 2) {
return "factset: recursion depth exceeded.\n";
}
return $channel if not defined $trigger; # if $trigger is not defined, $channel is an error message return $channel if not defined $trigger; # if $trigger is not defined, $channel is an error message
my $trigger_name = $self->{pbot}->{factoids}->{data}->{storage}->get_data($channel, $trigger, '_name'); my $trigger_name = $self->{pbot}->{factoids}->{data}->{storage}->get_data($channel, $trigger, '_name');
@ -372,6 +393,10 @@ sub cmd_factset($self, $context) {
return "/say $trigger_name is locked; unlock before setting."; return "/say $trigger_name is locked; unlock before setting.";
} }
if ($channel =~ /^#/ && $channel ne $context->{from} && !$self->{pbot}->{capabilities}->userhas($userinfo, 'admin')) {
return "/say Switch to $channel to factset this factoid.";
}
if (lc $key eq 'cap-override' and defined $value) { if (lc $key eq 'cap-override' and defined $value) {
if (not $self->{pbot}->{capabilities}->exists($value)) { if (not $self->{pbot}->{capabilities}->exists($value)) {
return "No such capability $value."; return "No such capability $value.";
@ -403,6 +428,10 @@ sub cmd_factset($self, $context) {
sub cmd_factunset($self, $context) { sub cmd_factunset($self, $context) {
my $usage = 'Usage: factunset [channel] <factoid> <key>'; my $usage = 'Usage: factunset [channel] <factoid> <key>';
if ($context->{interpret_depth} > 2) {
return "factunset: recursion depth exceeded.\n";
}
my ($channel, $trigger, $arguments) = $self->find_factoid_with_optional_channel( my ($channel, $trigger, $arguments) = $self->find_factoid_with_optional_channel(
$context->{from}, $context->{arguments}, 'factunset', usage => $usage, explicit => 1 $context->{from}, $context->{arguments}, 'factunset', usage => $usage, explicit => 1
); );
@ -457,6 +486,10 @@ sub cmd_factunset($self, $context) {
return "[$channel_name] $trigger_name: key '$key' does not exist."; return "[$channel_name] $trigger_name: key '$key' does not exist.";
} }
if ($channel =~ /^#/ && $channel ne $context->{from}) {
return "/say Switch to $channel to factunset this factoid.";
}
if ($key eq 'cap-override') { if ($key eq 'cap-override') {
if (not $self->{pbot}->{capabilities}->userhas($userinfo, $oldvalue)) { if (not $self->{pbot}->{capabilities}->userhas($userinfo, $oldvalue)) {
return "Your user account must have the $oldvalue capability to unset this cap-override."; return "Your user account must have the $oldvalue capability to unset this cap-override.";
@ -475,6 +508,10 @@ sub cmd_factunset($self, $context) {
sub cmd_factmove($self, $context) { sub cmd_factmove($self, $context) {
my ($src_channel, $source, $target_channel, $target) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 5); my ($src_channel, $source, $target_channel, $target) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 5);
if ($context->{interpret_depth} > 2) {
return "factmove: recursion depth exceeded.\n";
}
my $usage = "Usage: factmove <source channel> <source factoid> <target channel/factoid> [target factoid]"; my $usage = "Usage: factmove <source channel> <source factoid> <target channel/factoid> [target factoid]";
return $usage if not defined $target_channel; return $usage if not defined $target_channel;
@ -557,6 +594,10 @@ sub cmd_factmove($self, $context) {
$target_channel = '.*' if $target_channel !~ /^#/; $target_channel = '.*' if $target_channel !~ /^#/;
if ($target_channel =~ /^#/ && $target_channel ne $context->{from}) {
return "/say Switch to $target_channel to move this factoid.";
}
my $data = $factoids->get_data($found_src_channel, $found_source); my $data = $factoids->get_data($found_src_channel, $found_source);
$factoids->remove($found_src_channel, $found_source, undef, 1); $factoids->remove($found_src_channel, $found_source, undef, 1);
$factoids->add($target_channel, $target, $data); $factoids->add($target_channel, $target, $data);
@ -578,6 +619,10 @@ sub cmd_factmove($self, $context) {
sub cmd_factcopy($self, $context) { sub cmd_factcopy($self, $context) {
my ($src_channel, $source, $target_channel, $target) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 5); my ($src_channel, $source, $target_channel, $target) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 5);
if ($context->{interpret_depth} > 2) {
return "factcopy: recursion depth exceeded.\n";
}
my $usage = "Usage: factcopy <source channel> <source factoid> <target channel/factoid> [target factoid]"; my $usage = "Usage: factcopy <source channel> <source factoid> <target channel/factoid> [target factoid]";
return $usage if not defined $target_channel; return $usage if not defined $target_channel;
@ -654,6 +699,10 @@ sub cmd_factcopy($self, $context) {
$target_channel = '.*' if $target_channel !~ /^#/; $target_channel = '.*' if $target_channel !~ /^#/;
if ($target_channel =~ /^#/ && $target_channel ne $context->{from}) {
return "/say Switch to $target_channel to move this factoid.";
}
my $data = $factoids->get_data($found_src_channel, $found_source); my $data = $factoids->get_data($found_src_channel, $found_source);
$data->{owner} = $context->{hostmask}; $data->{owner} = $context->{hostmask};
@ -679,6 +728,11 @@ sub cmd_factcopy($self, $context) {
sub cmd_factalias($self, $context) { sub cmd_factalias($self, $context) {
my ($chan, $alias, $command) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 3, 0, 1); my ($chan, $alias, $command) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 3, 0, 1);
if ($context->{interpret_depth} > 2) {
return "factalias: recursion depth exceeded.\n";
}
if (defined $chan and not($chan eq '.*' or $chan =~ m/^#/)) { if (defined $chan and not($chan eq '.*' or $chan =~ m/^#/)) {
# $chan doesn't look like a channel, so shift everything to the right # $chan doesn't look like a channel, so shift everything to the right
# and replace $chan with $from # and replace $chan with $from
@ -713,6 +767,12 @@ sub cmd_factalias($self, $context) {
if ($self->{pbot}->{commands}->exists($alias)) { return "/say $alias already exists as a built-in command."; } if ($self->{pbot}->{commands}->exists($alias)) { return "/say $alias already exists as a built-in command."; }
my $userinfo = $self->{pbot}->{users}->loggedin($chan, $context->{hostmask});
if (!$self->{pbot}->{capabilities}->userhas($userinfo, 'admin') && $chan =~ /^#/ && $chan ne $context->{from}) {
return "/say Switch to $chan to add this factoid.";
}
$self->{pbot}->{factoids}->{data}->add('text', $chan, $context->{hostmask}, $alias, "/call $command"); $self->{pbot}->{factoids}->{data}->add('text', $chan, $context->{hostmask}, $alias, "/call $command");
$self->{pbot}->{logger}->log("$context->{hostmask} [$chan] aliased $alias => $command\n"); $self->{pbot}->{logger}->log("$context->{hostmask} [$chan] aliased $alias => $command\n");
return "/say $alias aliases `$command` for " . ($chan eq '.*' ? 'the global channel' : $chan); return "/say $alias aliases `$command` for " . ($chan eq '.*' ? 'the global channel' : $chan);
@ -755,11 +815,16 @@ my @valid_pastesites = (
'https?://ix.io', 'https?://ix.io',
'https?://dpaste.com', 'https?://dpaste.com',
'https?://0x0.st', 'https?://0x0.st',
'https?://x0.at',
); );
sub cmd_factadd($self, $context) { sub cmd_factadd($self, $context) {
my ($from_chan, $keyword, $text, $force); my ($from_chan, $keyword, $text, $force);
if ($context->{interpret_depth} > 2) {
return "factadd: recursion depth exceeded.\n";
}
my @arglist = @{$context->{arglist}}; my @arglist = @{$context->{arglist}};
if (@arglist) { if (@arglist) {
@ -872,6 +937,12 @@ sub cmd_factadd($self, $context) {
if ($self->{pbot}->{commands}->exists($keyword)) { return "/say $keyword_text already exists as a built-in command."; } if ($self->{pbot}->{commands}->exists($keyword)) { return "/say $keyword_text already exists as a built-in command."; }
my $userinfo = $self->{pbot}->{users}->loggedin($from_chan, $context->{hostmask});
if (!$self->{pbot}->{capabilities}->userhas($userinfo, 'admin') && $from_chan =~ /^#/ && $from_chan ne $context->{from}) {
return "/say Switch to $from_chan to add this factoid.";
}
my $exists = $self->{pbot}->{factoids}->{data}->{storage}->exists($from_chan, $keyword); my $exists = $self->{pbot}->{factoids}->{data}->{storage}->exists($from_chan, $keyword);
$self->{pbot}->{factoids}->{data}->add('text', $from_chan, $context->{hostmask}, $keyword, $text); $self->{pbot}->{factoids}->{data}->add('text', $from_chan, $context->{hostmask}, $keyword, $text);
@ -892,6 +963,10 @@ sub cmd_factadd($self, $context) {
sub cmd_factrem($self, $context) { sub cmd_factrem($self, $context) {
my $factoids = $self->{pbot}->{factoids}->{data}->{storage}; my $factoids = $self->{pbot}->{factoids}->{data}->{storage};
if ($context->{interpret_depth} > 2) {
return "factrem: recursion depth exceeded.\n";
}
my ($from_chan, $from_trig) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2); my ($from_chan, $from_trig) = $self->{pbot}->{interpreter}->split_args($context->{arglist}, 2);
if (not defined $from_trig) { if (not defined $from_trig) {
@ -910,7 +985,15 @@ sub cmd_factrem($self, $context) {
$channel_name = 'global' if $channel_name eq '.*'; $channel_name = 'global' if $channel_name eq '.*';
$trigger_name = "\"$trigger_name\"" if $trigger_name =~ / /; $trigger_name = "\"$trigger_name\"" if $trigger_name =~ / /;
if ($factoids->get_data($channel, $trigger, 'type') eq 'applet') { return "/say $trigger_name is not a factoid."; } if ($factoids->get_data($channel, $trigger, 'type') eq 'applet') {
return "/say $trigger_name is not a factoid.";
}
my $is_admin = $self->{pbot}->{users}->loggedin_admin($channel, $context->{hostmask});
if (!$is_admin && $channel =~ /^#/ && $channel ne $context->{from}) {
return "/say Switch to $channel_name to remove this factoid.";
}
if ($channel =~ /^#/ and $from_chan =~ /^#/ and lc $channel ne lc $from_chan) { if ($channel =~ /^#/ and $from_chan =~ /^#/ and lc $channel ne lc $from_chan) {
return "/say $trigger_name belongs to $channel_name, but this is $from_chan. Please switch to $channel_name or use /msg to remove this factoid."; return "/say $trigger_name belongs to $channel_name, but this is $from_chan. Please switch to $channel_name or use /msg to remove this factoid.";
@ -1275,6 +1358,10 @@ sub cmd_factchange($self, $context) {
my $factoids_data = $self->{pbot}->{factoids}->{data}->{storage}; my $factoids_data = $self->{pbot}->{factoids}->{data}->{storage};
my ($channel, $trigger, $keyword, $delim, $tochange, $changeto, $modifier, $url); my ($channel, $trigger, $keyword, $delim, $tochange, $changeto, $modifier, $url);
if ($context->{interpret_depth} > 2) {
return "factchange: recursion depth exceeded.\n";
}
my $needs_disambig; my $needs_disambig;
if (length $context->{arguments}) { if (length $context->{arguments}) {
@ -1361,11 +1448,16 @@ sub cmd_factchange($self, $context) {
$from_chan = '.*' if $from_chan eq 'global'; $from_chan = '.*' if $from_chan eq 'global';
my $userinfo = $self->{pbot}->{users}->loggedin($channel, $context->{hostmask});
if ($channel =~ /^#/ && $channel ne $context->{from} && !$self->{pbot}->{capabilities}->userhas($userinfo, 'admin')) {
return "/say Switch to $channel_name to change this factoid.";
}
if ($channel =~ /^#/ and $from_chan =~ /^#/ and lc $channel ne lc $from_chan) { if ($channel =~ /^#/ and $from_chan =~ /^#/ and lc $channel ne lc $from_chan) {
return "/say $trigger_name belongs to $channel_name, but this is $from_chan. Please switch to $channel_name or use /msg to change this factoid."; return "/say $trigger_name belongs to $channel_name, but this is $from_chan. Please switch to $channel_name or use /msg to change this factoid.";
} }
my $userinfo = $self->{pbot}->{users}->loggedin($channel, $context->{hostmask});
if ($factoids_data->get_data($channel, $trigger, 'locked')) { if ($factoids_data->get_data($channel, $trigger, 'locked')) {
return "/say $trigger_name is locked and cannot be changed." if not $self->{pbot}->{capabilities}->userhas($userinfo, 'admin'); return "/say $trigger_name is locked and cannot be changed." if not $self->{pbot}->{capabilities}->userhas($userinfo, 'admin');

View File

@ -236,10 +236,6 @@ sub expand_action_arguments($self, $context, $action, $input = '', $nick = '') {
$action = validate_string($action, $self->{pbot}->{registry}->get_value('factoids', 'max_content_length')); $action = validate_string($action, $self->{pbot}->{registry}->get_value('factoids', 'max_content_length'));
$input = validate_string($input, $self->{pbot}->{registry}->get_value('factoids', 'max_content_length')); $input = validate_string($input, $self->{pbot}->{registry}->get_value('factoids', 'max_content_length'));
my %opts = (
nested => 0,
);
my @args = $self->{pbot}->{interpreter}->split_line($input); my @args = $self->{pbot}->{interpreter}->split_line($input);
$action =~ s/\$arglen\b|\$\{arglen\}/scalar @args/eg; $action =~ s/\$arglen\b|\$\{arglen\}/scalar @args/eg;

View File

@ -271,6 +271,7 @@ sub process_line($self, $from, $nick, $user, $host, $text, $tags = '', $is_comma
# takes a $context object containing contextual information about the # takes a $context object containing contextual information about the
# command such as the channel, nick, user, host, command, etc. # command such as the channel, nick, user, host, command, etc.
sub interpret($self, $context) { sub interpret($self, $context) {
$context->{stack_depth} //= 0;
# log command invocation # log command invocation
$self->{pbot}->{logger}->log("=== [$context->{interpret_depth} ($context->{stack_depth})] Got command: " $self->{pbot}->{logger}->log("=== [$context->{interpret_depth} ($context->{stack_depth})] Got command: "
. "($context->{from}) $context->{hostmask}: $context->{command}\n"); . "($context->{from}) $context->{hostmask}: $context->{command}\n");
@ -633,7 +634,7 @@ sub handle_result($self, $context, $result = $context->{result}) {
} }
# finish piping # finish piping
if (exists $context->{pipe}->{$context->{stack_depth}}) { if (exists $context->{pipe} && exists $context->{pipe}->{$context->{stack_depth}}) {
my ($pipe, $pipe_rest) = ( my ($pipe, $pipe_rest) = (
delete $context->{pipe}->{$context->{stack_depth}}, delete $context->{pipe}->{$context->{stack_depth}},
delete $context->{pipe_rest}->{$context->{stack_depth}} delete $context->{pipe_rest}->{$context->{stack_depth}}

View File

@ -25,8 +25,8 @@ use PBot::Imports;
# These are set by the /misc/update_version script # These are set by the /misc/update_version script
use constant { use constant {
BUILD_NAME => "PBot", BUILD_NAME => "PBot",
BUILD_REVISION => 4882, BUILD_REVISION => 4884,
BUILD_DATE => "2025-08-25", BUILD_DATE => "2025-09-04",
}; };
sub initialize {} sub initialize {}