forked from github/kensanata.oddmuse
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0974b7bbd8 | ||
|
|
f73d420957 | ||
|
|
17ef2aaf88 | ||
|
|
b70c8e8def | ||
|
|
f8752e69bc | ||
|
|
9d48f875a2 | ||
|
|
39e9cea7b0 | ||
|
|
e7b718f610 | ||
|
|
261aeccb3f | ||
|
|
a09c846700 | ||
|
|
8dbede3813 | ||
|
|
89d9f27b2a |
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2004, 2005, 2006 Alex Schroeder <alex@emacswiki.org>
|
||||
# Copyright (C) 2004–2023 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2006 Ingo Belka
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
@@ -112,9 +112,7 @@ sub DoCollect {
|
||||
my $search = GetParam('search', '');
|
||||
ReportError(T('The match parameter is missing.')) unless $match or $search;
|
||||
print GetHeader('', Ts('Page Collection for %s', $match||$search), '');
|
||||
my @pages = (grep(/$match/, $search
|
||||
? SearchTitleAndBody($search)
|
||||
: AllPagesList()));
|
||||
my @pages = Matched($match, $search ? SearchTitleAndBody($search) : AllPagesList());
|
||||
if (!$CollectingJournal) {
|
||||
$CollectingJournal = 1;
|
||||
# Now save information required for saving the cache of the current page.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2004, 2007 Alex Schroeder <alex@emacswiki.org>
|
||||
# Copyright (C) 2004–2023 Alex Schroeder <alex@gnu.org>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -47,8 +47,5 @@ sub PrintableIndexPages {
|
||||
push(@pages, AllPagesList()) if GetParam('pages', 1);
|
||||
push(@pages, keys %PermanentAnchors) if GetParam('permanentanchors', 1);
|
||||
push(@pages, keys %NearSource) if GetParam('near', 0);
|
||||
my $match = GetParam('match', '');
|
||||
@pages = grep /$match/i, @pages if $match;
|
||||
@pages = sort @pages;
|
||||
return @pages;
|
||||
return sort Matched(GetParam('match'), @pages);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2004–2021 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2004–2023 Alex Schroeder <alex@gnu.org>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -86,7 +86,7 @@ sub JournalRssGetRcLines {
|
||||
my $reverse = GetParam('reverse', 0);
|
||||
my $monthly = GetParam('monthly', 0);
|
||||
my $offset = GetParam('offset', 0);
|
||||
my @pages = sort JournalSort (grep(/$match/, $search ? SearchTitleAndBody($search) : AllPagesList()));
|
||||
my @pages = sort JournalSort (Matched($match, $search ? SearchTitleAndBody($search) : AllPagesList()));
|
||||
if ($monthly and not $match) {
|
||||
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime();
|
||||
$match = '^' . sprintf("%04d-%02d", $year+1900, $mon+1) . '-\d\d';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2009–2020 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2009–2022 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2015 Aleks-Daniel Jakimenko <alex.jakimenko@gmail.com>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#! /usr/bin/perl
|
||||
# Copyright (C) 2014–2019 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2014–2022 Alex Schroeder <alex@gnu.org>
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
@@ -194,12 +194,11 @@ sub MarkdownRule {
|
||||
return OpenHtmlEnvironment('pre',1) . $str; # always level 1
|
||||
}
|
||||
# link: [an example](http://example.com/ "Title")
|
||||
elsif (m/\G\[((?:[^]\n]+\n?)+)\]\($FullUrlPattern(\s+"(.+?)")?\)/cg) {
|
||||
elsif (m/\G\[((?:[^]\n]+\n?)+)\]\((\S+)(\s+"(.+?)")?\)/cg) {
|
||||
my ($text, $url, $title) = ($1, $2, $4);
|
||||
$url =~ /^($UrlProtocols)/;
|
||||
my %params;
|
||||
$params{-href} = $url;
|
||||
$params{-class} = "url $1";
|
||||
$params{-class} = "url";
|
||||
$params{-title} = $title if $title;
|
||||
return $q->a(\%params, $text);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2012 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2004–2022 Alex Schroeder <alex@gnu.org>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
|
||||
198
modules/network-blocker.pl
Normal file
198
modules/network-blocker.pl
Normal file
@@ -0,0 +1,198 @@
|
||||
# -*- mode: perl -*-
|
||||
# Copyright (C) 2023 Alex Schroeder <alex@gnu.org>
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, either version 3 of the License, or (at your option) any
|
||||
# later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
=encoding utf8
|
||||
|
||||
=head1 NAME
|
||||
|
||||
Oddmuse Network Blocker
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This module hooks into regular Oddmuse Surge Protection. It adds the following
|
||||
features:
|
||||
|
||||
Repeated offenders are blocked for increasingly longer times.
|
||||
|
||||
For every offender, we record the CIDR their IP number belongs to. Everytime an
|
||||
IP number is blocked, all the CIDRs of the other blocked IPs are checked: if
|
||||
there are three or more blocked IP numbers sharing the same CIDRs, the CIDR
|
||||
itself is blocked.
|
||||
|
||||
CIDR blocking works the same way: Repeated offenders are blocked for
|
||||
increasingly longer times.
|
||||
|
||||
=head2 Behind a reverse proxy
|
||||
|
||||
Make sure your config file copies the IP number to the correct environment
|
||||
variable:
|
||||
|
||||
$ENV{REMOTE_ADDR} = $ENV{HTTP_X_FORWARDED_FOR};
|
||||
|
||||
=head1 SEE ALSO
|
||||
|
||||
<Oddmuse Surge Protection|https://oddmuse.org/wiki/Surge_Protection>
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use v5.10;
|
||||
use Net::IP qw(:PROC);
|
||||
use Net::DNS qw(rr);
|
||||
|
||||
AddModuleDescription('network-blocker.pl', 'Network Blocker Extension');
|
||||
|
||||
our ($Now, $DataDir, $SurgeProtectionViews, $SurgeProtectionTime);
|
||||
|
||||
{
|
||||
no warnings 'redefine';
|
||||
*OldNetworkBlockerDelayRequired = \&DelayRequired;
|
||||
*DelayRequired = \&NewNetworkBlockerDelayRequired;
|
||||
}
|
||||
|
||||
# Block for at least this many seconds.
|
||||
my $NetworkBlockerMinimumPeriod = 30;
|
||||
|
||||
# Every violation doubles the current period until this maximum is reached (four weeks).
|
||||
my $NetworkBlockerMaximumPeriod = 60 * 60 * 24 * 7 * 4;
|
||||
|
||||
# All the blocked networks. Maps CIDR to an array [expiry timestamp, expiry
|
||||
# period].
|
||||
my %NetworkBlockerList;
|
||||
|
||||
# Candidates are remembered for this many seconds.
|
||||
my $NetworkBlockerCachePeriod = 600;
|
||||
|
||||
# All the candidate networks for a block. Maps IP to an array [ts, cidr, ...].
|
||||
# Candidates are removed after $NetworkBlockerCachePeriod.
|
||||
my %NetworkBlockerCandidates;
|
||||
|
||||
sub NetworkBlockerRead {
|
||||
my ($status, $data) = ReadFile("$DataDir/network-blocks");
|
||||
return unless $status;
|
||||
my @lines = split(/\n/, $data);
|
||||
while ($_ = shift(@lines)) {
|
||||
my @items = split(/,/);
|
||||
$NetworkBlockerList{shift(@items)} = \@items;
|
||||
}
|
||||
# an empty line separates the two sections
|
||||
while ($_ = shift(@lines)) {
|
||||
my @items = split(/,/);
|
||||
$NetworkBlockerCandidates{shift(@items)} = \@items;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub NetworkBlockerWrite {
|
||||
RequestLockDir('network-blocks') or return '';
|
||||
WriteStringToFile(
|
||||
"$DataDir/network-blocks",
|
||||
join("\n\n",
|
||||
join("\n", map {
|
||||
join(",", $_, @{$NetworkBlockerList{$_}})
|
||||
} keys %NetworkBlockerList),
|
||||
join("\n", map {
|
||||
join(",", $_, @{$NetworkBlockerCandidates{$_}})
|
||||
} keys %NetworkBlockerCandidates)));
|
||||
ReleaseLockDir('network-blocks');
|
||||
}
|
||||
|
||||
sub NewNetworkBlockerDelayRequired {
|
||||
my $ip = shift;
|
||||
# If $ip is a name and not an IP number, parsing fails. In this case, run the
|
||||
# regular code.
|
||||
my $ob = new Net::IP($ip);
|
||||
return OldNetworkBlockerDelayRequired($ip) unless $ob;
|
||||
# Read the file. If the file does not exist, no problem.
|
||||
NetworkBlockerRead();
|
||||
# See if the current IP number is one of the blocked CIDR ranges.
|
||||
for my $cidr (keys %NetworkBlockerList) {
|
||||
# Perhaps this CIDR block can be expired.
|
||||
if ($NetworkBlockerList{$cidr}->[0] < $Now) {
|
||||
delete $NetworkBlockerList{$cidr};
|
||||
next;
|
||||
}
|
||||
# Forget the CIDR if it cannot be turned into a range.
|
||||
my $range = new Net::IP($cidr);
|
||||
if (not $range) {
|
||||
warn "CIDR $cidr is blocked but has no range: " . Net::IP::Error();
|
||||
delete $NetworkBlockerList{$cidr};
|
||||
next;
|
||||
}
|
||||
# If the CIDR overlaps with the remote IP number, it's a block.
|
||||
warn "Checking whether $ip is in $cidr\n";
|
||||
my $overlap = $range->overlaps($ob);
|
||||
# $IP_PARTIAL_OVERLAP (ranges overlap) $IP_NO_OVERLAP (no overlap)
|
||||
# $IP_A_IN_B_OVERLAP (range2 contains range1) $IP_B_IN_A_OVERLAP (range1
|
||||
# contains range2) $IP_IDENTICAL (ranges are identical) undef (problem)
|
||||
if (defined $overlap and $overlap != $IP_NO_OVERLAP) {
|
||||
# Double the block period unless it has reached $NetworkBlockerMaximumPeriod.
|
||||
if ($NetworkBlockerList{$cidr}->[1] < $NetworkBlockerMaximumPeriod / 2) {
|
||||
$NetworkBlockerList{$cidr}->[1] *= 2;
|
||||
} else {
|
||||
$NetworkBlockerList{$cidr}->[1] = $NetworkBlockerMaximumPeriod;
|
||||
}
|
||||
$NetworkBlockerList{$cidr}->[0] = $Now + $NetworkBlockerList{$cidr}->[1];
|
||||
# And we're done!
|
||||
NetworkBlockerWrite();
|
||||
ReportError(Ts('Too many connections by %s', $cidr)
|
||||
. ': ' . Tss('Please do not fetch more than %1 pages in %2 seconds.',
|
||||
$SurgeProtectionViews, $SurgeProtectionTime),
|
||||
'503 SERVICE UNAVAILABLE');
|
||||
}
|
||||
}
|
||||
# If the CIDR isn't blocked, let's see if Surge Protection wants to block it.
|
||||
my $result = OldNetworkBlockerDelayRequired($ip);
|
||||
warn "$ip was blocked\n" if $result;
|
||||
# If the IP is to be blocked, determine its CIDRs and put them on a list. Sadly,
|
||||
# routeviews does not support IPv6 at the moment!
|
||||
if ($result and not ip_is_ipv6($ip) and not $NetworkBlockerCandidates{$ip}) {
|
||||
my $reverse = $ob->reverse_ip();
|
||||
$reverse =~ s/in-addr\.arpa\.$/asn.routeviews.org/;
|
||||
my @candidates;
|
||||
for my $rr (rr($reverse, "TXT")) {
|
||||
next unless $rr->type eq "TXT";
|
||||
my @data = $rr->txtdata;
|
||||
push(@candidates, join("/", @data[1..2]));
|
||||
}
|
||||
warn "$ip is in @candidates\n";
|
||||
$NetworkBlockerCandidates{$ip} = [$Now, @candidates];
|
||||
# Expire any of the other candidates
|
||||
for my $other_ip (keys %NetworkBlockerCandidates) {
|
||||
if ($NetworkBlockerCandidates{$other_ip}->[0] < $Now - $NetworkBlockerCachePeriod) {
|
||||
delete $NetworkBlockerCandidates{$other_ip};
|
||||
}
|
||||
}
|
||||
# Determine if any of the CIDRs is to be blocked.
|
||||
my $save;
|
||||
for my $cidr (@candidates) {
|
||||
# Count how often the candidate CIDRs show up for other IP numbers.
|
||||
my $count = 0;
|
||||
for my $other_ip (keys %NetworkBlockerCandidates) {
|
||||
my @data = $NetworkBlockerCandidates{$other_ip};
|
||||
for my $other_cidr (@data[1 .. $#data]) {
|
||||
$count++ if $cidr eq $other_cidr;
|
||||
}
|
||||
}
|
||||
if ($count >= 3) {
|
||||
$NetworkBlockerList{$cidr} = [$Now + $NetworkBlockerMinimumPeriod, $NetworkBlockerMinimumPeriod];
|
||||
$save = 1;
|
||||
}
|
||||
}
|
||||
NetworkBlockerWrite() if $save;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2019 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2019–2023 Alex Schroeder <alex@gnu.org>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -65,6 +65,7 @@ sub RenamePageMenu {
|
||||
. GetHiddenValue('id', $id)
|
||||
. $q->textfield(-name=>'to', -size=>20)
|
||||
. ' '
|
||||
. $q->submit('Do it'));
|
||||
. $q->submit('Do it')
|
||||
. $q->end_form());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2006, 2007, 2008 Alex Schroeder <alex@emacswiki.org>
|
||||
# Copyright (C) 2006–2023 Alex Schroeder <alex@gnu.org>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -36,18 +36,12 @@ sub SearchListRule {
|
||||
$term = GetId();
|
||||
}
|
||||
local ($OpenPageName, %Page);
|
||||
my %hash = ();
|
||||
my @found;
|
||||
if ($variation eq 'list') {
|
||||
foreach my $id (SearchTitleAndBody($term)) {
|
||||
$hash{$id} = 1 unless $id eq $original; # skip the page with the query
|
||||
}
|
||||
@found = grep { $_ ne $original } SearchTitleAndBody($term);
|
||||
} elsif ($variation eq 'titlelist') {
|
||||
@found = grep { $_ ne $original } Matched($term, AllPagesList());
|
||||
}
|
||||
if ($variation eq 'titlelist') {
|
||||
foreach my $id (grep(/$term/, AllPagesList())) {
|
||||
$hash{$id} = 1 unless $id eq $original; # skip the page with the query
|
||||
}
|
||||
}
|
||||
my @found = keys %hash;
|
||||
if (defined &PageSort) {
|
||||
@found = sort PageSort @found;
|
||||
} else {
|
||||
@@ -63,32 +57,24 @@ sub SearchListRule {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
# Add a new action list
|
||||
|
||||
$Action{list} = \&DoList;
|
||||
|
||||
sub DoList {
|
||||
my $id = shift;
|
||||
my $match = GetParam('match', '');
|
||||
my $search = GetParam('search', '');
|
||||
my $id = shift;
|
||||
my $match = GetParam('match', '');
|
||||
my $search = GetParam('search', '');
|
||||
ReportError(T('The search parameter is missing.')) unless $match or $search;
|
||||
print GetHeader('', Ts('Page list for %s', $match||$search), '');
|
||||
local (%Page, $OpenPageName);
|
||||
my %hash = ();
|
||||
foreach my $id (grep(/$match/, $search
|
||||
? SearchTitleAndBody($search)
|
||||
: AllPagesList())) {
|
||||
$hash{$id} = 1;
|
||||
}
|
||||
my @found = keys %hash;
|
||||
if (defined &PageSort) {
|
||||
@found = sort PageSort @found;
|
||||
} else {
|
||||
@found = sort(@found);
|
||||
}
|
||||
@found = map { $q->li(GetPageLink($_)) } @found;
|
||||
print $q->start_div({-class=>'search list'}),
|
||||
$q->ul(@found), $q->end_div;
|
||||
my @found = Matched($match, $search ? SearchTitleAndBody($search) : AllPagesList());
|
||||
if (defined &PageSort) {
|
||||
@found = sort PageSort @found;
|
||||
} else {
|
||||
@found = sort(@found);
|
||||
}
|
||||
@found = map { $q->li(GetPageLink($_)) } @found;
|
||||
print $q->start_div({-class=>'search list'}), $q->ul(@found), $q->end_div;
|
||||
PrintFooter();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2007–2014 Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2007–2023 Alex Schroeder <alex@gnu.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
@@ -36,8 +36,7 @@ sub SisterPages {
|
||||
push(@pages, AllPagesList()) if GetParam('pages', 1);
|
||||
push(@pages, keys %PermanentAnchors) if GetParam('permanentanchors', 1);
|
||||
push(@pages, keys %NearSource) if GetParam('near', 0);
|
||||
my $match = GetParam('match', '');
|
||||
@pages = grep /$match/i, @pages if $match;
|
||||
@pages = Matched(GetParam('match', ''), @pages);
|
||||
@pages = sort @pages;
|
||||
return @pages;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
use strict;
|
||||
use v5.10;
|
||||
|
||||
our (@MyInitVariables, $HtmlHeaders);
|
||||
our (@MyInitVariables, $HtmlHeaders, $EditNote);
|
||||
|
||||
AddModuleDescription('wordcount.pl', 'Word Count Extension');
|
||||
|
||||
@@ -57,3 +57,5 @@ sub WordcountAddScript {
|
||||
}
|
||||
</script>";
|
||||
}
|
||||
|
||||
$EditNote = "Words: <span id='textWordCount'></span>" . $EditNote;
|
||||
|
||||
@@ -85,7 +85,9 @@ versions of Oddmuse.</p>
|
||||
<ul>
|
||||
% for my $tarball (@$tarballs) {
|
||||
<li>
|
||||
% if ($tarball ne 'latest') {
|
||||
<a href="https://oddmuse.org/releases/<%= $tarball %>.tar.gz"><%= $tarball %>.tar.gz</a>
|
||||
% }
|
||||
(files for <%= link_to release => {tarball => $tarball} => begin %>\
|
||||
<%= $tarball =%><%= end %>)
|
||||
</li>
|
||||
|
||||
@@ -137,13 +137,13 @@ EOT
|
||||
|
||||
xpath_run_tests(split(/\n/,<<'EOT'));
|
||||
[example](http://example.com/)
|
||||
//a[@class="url http"][@href="http://example.com/"][text()="example"]
|
||||
//a[@class="url"][@href="http://example.com/"][text()="example"]
|
||||
[an example](http://example.com/)
|
||||
//a[@class="url http"][@href="http://example.com/"][text()="an example"]
|
||||
//a[@class="url"][@href="http://example.com/"][text()="an example"]
|
||||
[an example](http://example.com/ "Title")
|
||||
//a[@class="url http"][@href="http://example.com/"][@title="Title"][text()="an example"]
|
||||
//a[@class="url"][@href="http://example.com/"][@title="Title"][text()="an example"]
|
||||
[an\nexample](http://example.com/)
|
||||
//a[@class="url http"][@href="http://example.com/"][text()="an\nexample"]
|
||||
//a[@class="url"][@href="http://example.com/"][text()="an\nexample"]
|
||||
\n[an\n\nexample](http://example.com/)
|
||||
//p[text()="[an"]/following-sibling::p//text()[contains(string(),"example](")]
|
||||
EOT
|
||||
|
||||
58
t/rollback-hang.t
Normal file
58
t/rollback-hang.t
Normal file
@@ -0,0 +1,58 @@
|
||||
# Copyright (C) 2006–2023 Alex Schroeder <alex@gnu.org>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify it under
|
||||
# the terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 3 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
require './t/test.pl';
|
||||
package OddMuse;
|
||||
use Test::More tests => 4;
|
||||
use utf8;
|
||||
|
||||
# Reproduce a particular bug from alexschroeder.ch with the rc.log provided.
|
||||
WriteStringToFile($RcFile, <<'EOT');
|
||||
16853910992023-05-29_Net_newsHow to IRCAnonymousAlex2en
|
||||
16854004152023-05-29_Net_newsHow to IRCAnonymousAlex3en
|
||||
1685430599[[rollback]]1685400415Anonymous
|
||||
16855185032023-05-29_Net_newsAnonymousAlex4en
|
||||
EOT
|
||||
|
||||
local $SIG{ALRM} = sub { fail "timeout!"; kill 'KILL', $$; };
|
||||
alarm 3;
|
||||
# this is recent changes from between the rollback and the page before it, so there are no pages to roll back
|
||||
my $page = get_page("action=rss full=1 short=0 from=1685413682");
|
||||
alarm 0;
|
||||
test_page($page, '2023-05-29 Net news');
|
||||
test_page_negative($page, 'rollback');
|
||||
|
||||
# Reproduce a follow-up bug. First, rolling back just Test works as intended.
|
||||
WriteStringToFile($RcFile, <<'EOT');
|
||||
1691499987Testham127.0.0.1Berta1
|
||||
1691499988Mustuff127.0.0.1Chris1
|
||||
1691499989Testspam127.0.0.1Spammer2
|
||||
1691499990Test0Rollback to 2023-08-08 13:06 UTC127.0.0.1Alex3
|
||||
1691499990[[rollback]]1691499987Test
|
||||
EOT
|
||||
|
||||
my $feed = get_page('action=rc raw=1');
|
||||
test_page($feed, 'title: Test');
|
||||
|
||||
# Rolling back all of the wiki doesn't work.
|
||||
WriteStringToFile($RcFile, <<'EOT');
|
||||
1691499987Testham127.0.0.1Berta1
|
||||
1691499988Mustuff127.0.0.1Chris1
|
||||
1691499989Testspam127.0.0.1Spammer2
|
||||
1691499990Test0Rollback to 2023-08-08 13:06 UTC127.0.0.1Alex3
|
||||
1691499990[[rollback]]1691499987
|
||||
EOT
|
||||
|
||||
$feed = get_page('action=rc raw=1');
|
||||
test_page($feed, 'title: Test');
|
||||
52
wiki.pl
52
wiki.pl
@@ -1,5 +1,5 @@
|
||||
#! /usr/bin/env perl
|
||||
# Copyright (C) 2001-2020
|
||||
# Copyright (C) 2001-2023
|
||||
# Alex Schroeder <alex@gnu.org>
|
||||
# Copyright (C) 2014-2015
|
||||
# Alex Jakimenko <alex.jakimenko@gmail.com>
|
||||
@@ -39,6 +39,7 @@ use B;
|
||||
use CGI qw/-utf8/;
|
||||
use CGI::Carp qw(fatalsToBrowser);
|
||||
use File::Glob ':glob';
|
||||
use List::Util qw(all max);
|
||||
use Encode qw(encode_utf8 decode_utf8);
|
||||
use sigtrap 'handler' => \&HandleSignals, 'normal-signals', 'error-signals';
|
||||
local $| = 1; # Do not buffer output (localized for mod_perl)
|
||||
@@ -1543,28 +1544,34 @@ sub LatestChanges {
|
||||
sub StripRollbacks {
|
||||
my @result = @_;
|
||||
if (not (GetParam('all', $ShowAll) or GetParam('rollback', $ShowRollbacks))) { # strip rollbacks
|
||||
my (%rollback);
|
||||
my (%rollback); # used for single-page rollbacks
|
||||
for (my $i = $#result; $i >= 0; $i--) {
|
||||
# some fields have a different meaning if looking at rollbacks
|
||||
my ($ts, $id, $target_ts, $target_id) = @{$result[$i]};
|
||||
# if this is a rollback marker
|
||||
if ($id eq '[[rollback]]') {
|
||||
# if this is a single page rollback marker, strip it
|
||||
if ($target_id) {
|
||||
$rollback{$target_id} = $target_ts; # single page rollback
|
||||
splice(@result, $i, 1); # strip marker
|
||||
# if this page is not already being rolled back, remember the target
|
||||
# id and target ts so that those lines can be stripped below
|
||||
if (not $rollback{$target_id} or $target_ts < $rollback{$target_id}) {
|
||||
$rollback{$target_id} = $target_ts;
|
||||
}
|
||||
# the marker is always stripped
|
||||
splice(@result, $i, 1);
|
||||
} else {
|
||||
# if this is a global rollback, things are different: we're going to
|
||||
# find the correct timestamp and strip all of those lines immediately
|
||||
my $end = $i;
|
||||
while ($ts > $target_ts and $i > 0) {
|
||||
$i--; # quickly skip all these lines
|
||||
$ts = $result[$i][0];
|
||||
}
|
||||
splice(@result, $i + 1, $end - $i);
|
||||
$i++; # compensate $i-- in for loop
|
||||
$i-- while $i > 0 and $target_ts < $result[$i-1][0];
|
||||
# splice the lines found
|
||||
splice(@result, $i, $end - $i + 1);
|
||||
}
|
||||
} elsif ($rollback{$id} and $ts > $rollback{$id}) {
|
||||
splice(@result, $i, 1); # strip rolled back single pages
|
||||
}
|
||||
}
|
||||
} else { # just strip the marker left by DoRollback()
|
||||
} else { # if rollbacks are not not shown, just strip the markers
|
||||
for (my $i = $#result; $i >= 0; $i--) {
|
||||
splice(@result, $i, 1) if $result[$i][1] eq '[[rollback]]'; # id
|
||||
}
|
||||
@@ -3368,7 +3375,6 @@ sub SortIndex {
|
||||
|
||||
sub DoIndex {
|
||||
my $raw = GetParam('raw', 0);
|
||||
my $match = GetParam('match', '');
|
||||
my $limit = GetParam('n', '');
|
||||
my @pages = ();
|
||||
my @menu = ($q->label({-for=>'indexmatch'}, T('Filter:')) . ' '
|
||||
@@ -3380,7 +3386,7 @@ sub DoIndex {
|
||||
push(@pages, $sub->()) if $value;
|
||||
push(@menu, $q->checkbox(-name=>$option, -checked=>$value, -label=>$text));
|
||||
}
|
||||
@pages = grep /$match/i, @pages if $match;
|
||||
@pages = Matched(GetParam('match', ''), @pages);
|
||||
@pages = sort SortIndex @pages;
|
||||
@pages = @pages[0 .. $limit - 1] if $limit;
|
||||
if ($raw) {
|
||||
@@ -3553,11 +3559,23 @@ sub SearchTitleAndBody {
|
||||
return @found;
|
||||
}
|
||||
|
||||
sub Filtered { # this is overwriten in extensions such as tags.pl
|
||||
# Filter the pages to be searched for $string. The default implementation
|
||||
# ignores $string and uses $match instead, just in case the user used both
|
||||
# search and match parameters. This is overwritten in extensions such as tags.pl
|
||||
# which extract tags from $string and use that to filter the pages.
|
||||
sub Filtered {
|
||||
my ($string, @pages) = @_;
|
||||
my $match = GetParam('match', '');
|
||||
@pages = grep /$match/i, @pages if $match;
|
||||
return @pages;
|
||||
return Matched(GetParam('match', ''), @pages);
|
||||
}
|
||||
|
||||
sub Matched { # strictly for page titles
|
||||
my ($string, @pages) = @_;
|
||||
return @pages unless $string;
|
||||
my @terms = grep { $_ } split(/[ _]+/, $string);
|
||||
return grep {
|
||||
my $id = $_;
|
||||
all { $id =~ /$_/i } @terms;
|
||||
} @pages;
|
||||
}
|
||||
|
||||
sub SearchString {
|
||||
|
||||
Reference in New Issue
Block a user