ban-contributors.pl 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. # Copyright (C) 2013-2016 Alex Schroeder <alex@gnu.org>
  2. # This program is free software: you can redistribute it and/or modify it under
  3. # the terms of the GNU General Public License as published by the Free Software
  4. # Foundation, either version 3 of the License, or (at your option) any later
  5. # version.
  6. #
  7. # This program is distributed in the hope that it will be useful, but WITHOUT
  8. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  9. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  10. #
  11. # You should have received a copy of the GNU General Public License along with
  12. # this program. If not, see <http://www.gnu.org/licenses/>.
  13. =head1 Ban Contributors Extension
  14. This module adds "Ban contributors" to the administration page. If you
  15. click on it, it will list all the recent contributors to the page
  16. you've been looking at. Each contributor (IP or hostname) will be
  17. compared to the list of regular expressions on the C<BannedHosts> page
  18. (see C<$BannedHosts>). If the contributor is already banned, this is
  19. mentioned. If the contributor is not banned, you'll see a button
  20. allowing you to ban him or her immediately. If you click the button,
  21. the IP will be added to the C<BannedHosts> page for you.
  22. =cut
  23. use strict;
  24. use v5.10;
  25. our ($q, $Now, %Page, $OpenPageName, %Action, $UrlPattern, $BannedContent, $BannedHosts, @MyAdminCode);
  26. AddModuleDescription('ban-contributors.pl', 'Ban Contributors Extension');
  27. push(@MyAdminCode, \&BanMenu);
  28. sub BanMenu {
  29. my ($id, $menuref, $restref) = @_;
  30. if ($id and UserIsAdmin()) {
  31. push(@$menuref, ScriptLink('action=ban;id=' . UrlEncode($id),
  32. T('Ban contributors')));
  33. }
  34. }
  35. $Action{ban} = \&DoBanHosts;
  36. sub IsItBanned {
  37. my ($it, $regexps) = @_;
  38. my $re = undef;
  39. foreach my $regexp (@$regexps) {
  40. eval { $re = qr/$regexp/i; };
  41. if (defined($re) && $it =~ $re) {
  42. return $it;
  43. }
  44. }
  45. }
  46. sub DoBanHosts {
  47. my $id = shift;
  48. my $content = GetParam('content', '');
  49. my $range = GetParam('range', '');
  50. my $regexp = GetParam('regexp', '');
  51. if ($content) {
  52. SetParam('text', GetPageContent($BannedContent)
  53. . $content . " # " . CalcDay($Now) . " "
  54. . NormalToFree($id) . "\n");
  55. SetParam('summary', NormalToFree($id));
  56. DoPost($BannedContent);
  57. } elsif ($regexp) {
  58. SetParam('text', GetPageContent($BannedHosts)
  59. . $regexp . " # " . CalcDay($Now)
  60. . " $range "
  61. . NormalToFree($id) . "\n");
  62. SetParam('summary', NormalToFree($id));
  63. DoPost($BannedHosts);
  64. } else {
  65. ValidIdOrDie($id);
  66. print GetHeader('', Ts('Ban Contributors to %s', NormalToFree($id)));
  67. SetParam('rcidonly', $id);
  68. SetParam('all', 1);
  69. SetParam('showedit', 1);
  70. my %contrib = ();
  71. for my $line (GetRcLines()) {
  72. $contrib{$line->[4]}->{$line->[5]} = 1 if $line->[4];
  73. }
  74. my @regexps = ();
  75. foreach (split(/\n/, GetPageContent($BannedHosts))) {
  76. if (/^\s*([^#]\S+)/) { # all lines except empty lines and comments, trim whitespace
  77. push(@regexps, $1);
  78. }
  79. }
  80. print '<div class="content ban">';
  81. foreach (sort(keys %contrib)) {
  82. my $name = $_;
  83. delete $contrib{$_}{''};
  84. $name .= " (" . join(", ", sort(keys(%{$contrib{$_}}))) . ")";
  85. if (IsItBanned($_, \@regexps)) {
  86. print $q->p(Ts("%s is banned", $name));
  87. } else {
  88. my ($start, $end) = BanContributors::get_range($_);
  89. $range = "[$start - $end]";
  90. $name .= " " . $range;
  91. print GetFormStart(undef, 'get', 'ban'),
  92. GetHiddenValue('action', 'ban'),
  93. GetHiddenValue('id', $id),
  94. GetHiddenValue('range', $range),
  95. GetHiddenValue('regexp', BanContributors::get_regexp_ip($start, $end)),
  96. GetHiddenValue('recent_edit', 'on'),
  97. $q->p($name, $q->submit(T('Ban!'))), $q->end_form();
  98. }
  99. }
  100. }
  101. PrintFooter();
  102. }
  103. =head2 Rollback
  104. If you are an admin and rolled back a single page, this extension will
  105. list the URLs your rollback removed (assuming that those URLs are part
  106. of the spam) and it will allow you to provide a regular expression
  107. that will be added to BannedHosts.
  108. =cut
  109. *OldBanContributorsWriteRcLog = \&WriteRcLog;
  110. *WriteRcLog = \&NewBanContributorsWriteRcLog;
  111. sub NewBanContributorsWriteRcLog {
  112. my ($tag, $id, $to) = @_;
  113. if ($tag eq '[[rollback]]' and $id and $to > 0
  114. and $OpenPageName eq $id and UserIsAdmin()) {
  115. # we currently have the clean page loaded, so we need to reload
  116. # the spammed revision (there is a possible race condition here)
  117. my $old = GetTextRevision($Page{revision} - 1, 1)->{text};
  118. my %urls = map {$_ => 1 } $old =~ /$UrlPattern/g;
  119. # we open the file again to force a load of the despammed page
  120. foreach my $url ($Page{text} =~ /$UrlPattern/g) {
  121. delete($urls{$url});
  122. }
  123. # we also remove any candidates that are already banned
  124. my @regexps = ();
  125. foreach (split(/\n/, GetPageContent($BannedContent))) {
  126. if (/^\s*([^#]\S+)/) { # all lines except empty lines and comments, trim whitespace
  127. push(@regexps, $1);
  128. }
  129. }
  130. foreach my $url (keys %urls) {
  131. delete($urls{$url}) if IsItBanned($url, \@regexps);
  132. }
  133. if (keys %urls) {
  134. print $q->p(Ts("These URLs were rolled back. Perhaps you want to add a regular expression to %s?",
  135. GetPageLink($BannedContent)));
  136. print $q->pre(join("\n", sort keys %urls));
  137. print GetFormStart(undef, 'get', 'ban'),
  138. GetHiddenValue('action', 'ban'),
  139. GetHiddenValue('id', $id),
  140. GetHiddenValue('recent_edit', 'on'),
  141. $q->p($q->label({-for=>'content'}, T('Regular expression:')), " ",
  142. $q->textfield(-name=>'content', -size=>30), " ",
  143. $q->submit(T('Ban!'))),
  144. $q->end_form();
  145. };
  146. print $q->p(T("Consider banning the IP number as well:"), ' ',
  147. ScriptLink('action=ban;id=' . UrlEncode($id), T('Ban contributors')));
  148. };
  149. return OldBanContributorsWriteRcLog(@_);
  150. }
  151. package BanContributors;
  152. use Net::Whois::Parser qw/parse_whois/;
  153. sub get_range {
  154. my $ip = shift;
  155. my $response = parse_whois(domain => $ip);
  156. my ($start, $end);
  157. my $re = '(?:[0-9]{1,3}\.){3}[0-9]{1,3}';
  158. my ($start, $end) = $response->{inetnum} =~ /($re) *- *($re)/;
  159. return $start, $end;
  160. }
  161. sub get_groups {
  162. my ($from, $to) = @_;
  163. my @groups;
  164. if ($from < 10) {
  165. my $to = $to >= 10 ? 9 : $to;
  166. push(@groups, [$from, $to]);
  167. $from = $to + 1;
  168. }
  169. while ($from < $to) {
  170. my $to = int($from/100) < int($to/100) ? $from + 99 - $from % 100 : $to;
  171. if ($from % 10) {
  172. push(@groups, [$from, $from + 9 - $from % 10]);
  173. $from += 10 - $from % 10;
  174. }
  175. if (int($from/10) < int($to/10)) {
  176. if ($to % 10 == 9) {
  177. push(@groups, [$from, $to]);
  178. $from = 1 + $to;
  179. } else {
  180. push(@groups, [$from, $to - 1 - $to % 10]);
  181. $from = $to - $to % 10;
  182. }
  183. } else {
  184. push(@groups, [$from - $from % 10, $to]);
  185. last;
  186. }
  187. if ($to % 10 != 9) {
  188. push(@groups, [$from, $to]);
  189. $from = 1 + $to; # jump from 99 to 100
  190. }
  191. }
  192. return \@groups;
  193. }
  194. sub get_regexp_range {
  195. my @chars;
  196. for my $group (@{get_groups(@_)}) {
  197. my ($from, $to) = @$group;
  198. my $char;
  199. for (my $i = length($from); $i >= 1; $i--) {
  200. if (substr($from, - $i, 1) eq substr($to, - $i, 1)) {
  201. $char .= substr($from, - $i, 1);
  202. } else {
  203. $char .= '[' . substr($from, - $i, 1) . '-' . substr($to, - $i, 1). ']';
  204. }
  205. }
  206. push(@chars, $char);
  207. }
  208. return join('|', @chars);
  209. }
  210. sub get_regexp_ip {
  211. my ($from, $to) = @_;
  212. my @start = split(/\./, $from);
  213. my @end = split(/\./, $to);
  214. my $regexp = "^";
  215. for my $i (0 .. 3) {
  216. if ($start[$i] eq $end[$i]) {
  217. $regexp .= $start[$i];
  218. } elsif ($start[$i] eq '0' and $end[$i] eq '255') {
  219. last;
  220. } elsif ($start[$i + 1] > 0) {
  221. $regexp .= '(' . $start[$i] . '\.('
  222. . get_regexp_range($start[$i + 1], '255') . ')|'
  223. . get_regexp_range($start[$i] + 1, $end[$i + 1]) . ')';
  224. $regexp .= '\.';
  225. last;
  226. } else {
  227. $regexp .= '(' . get_regexp_range($start[$i], $end[$i]) . ')$';
  228. last;
  229. }
  230. $regexp .= '\.' if $i < 3;
  231. }
  232. return $regexp;
  233. }
  234. # this is required in case we concatenate other modules to this one
  235. package OddMuse;