File Coverage

blib/lib/MediaWiki/Bot/Plugin/Steward.pm
Criterion Covered Total %
statement 21 140 15.0
branch 0 80 0.0
condition 0 33 0.0
subroutine 7 15 46.6
pod 5 5 100.0
total 33 273 12.0


line stmt bran cond sub pod time code
1             package MediaWiki::Bot::Plugin::Steward;
2             # ABSTRACT: A plugin to MediaWiki::Bot providing steward functions
3              
4 1     1   378160 use strict;
  1         3  
  1         56  
5 1     1   5 use warnings;
  1         3  
  1         33  
6              
7 1     1   6 use Carp;
  1         2  
  1         94  
8 1     1   1142 use Net::CIDR qw(range2cidr cidrvalidate);
  1         6602  
  1         309  
9 1     1   12 use URI::Escape qw(uri_escape_utf8);
  1         1  
  1         72  
10 1     1   1576 use WWW::Mechanize 1.30;
  1         160176  
  1         70  
11              
12             our $VERSION = 0.0003;
13              
14              
15              
16 1     1   11 use Exporter qw(import);
  1         2  
  1         2028  
17             our @EXPORT = qw(steward_new g_block g_unblock ca_lock ca_unlock _screenscrape_get _screenscrape_put _screenscrape_error _screenscrape_login);
18              
19              
20             sub steward_new {
21 0     0 1   my $self = shift;
22              
23 0           my $mech = WWW::Mechanize->new(
24             onerror => \&Carp::carp,
25             stack_depth => 1,
26             agent => $self->{'useragent'},
27             );
28 0           my $cookies = ".mediawiki-bot-$self->{'username'}-cookies";
29 0 0         if (-r $cookies) {
30 0           $mech->{'cookie_jar'}->load($cookies);
31 0           $mech->{'cookie_jar'}->{'ignore_discard'} = 1;
32             }
33             else {
34 0           croak "$cookies doesn't exist or isn't readable";
35             }
36 0           $self->{'mech'} = $mech;
37              
38 0           my $check_login = $self->_screenscrape_get('Special:BlankPage');
39 0 0         if ($check_login->decoded_content() =~ m/wgUserName="\Q$self->{'username'}\E"/) {
40 0           return 1;
41             }
42             else {
43 0           croak "Steward plugin couldn't log in";
44             }
45             }
46              
47              
48             sub g_block {
49 0     0 1   my $self = shift;
50 0 0         my $ip = ref $_[0] eq 'HASH' ? $_[0]->{'ip'} : shift; # Allow giving just an IP
51 0 0         my $ao = exists($_[0]->{'ao'}) ? $_[0]->{'ao'} : 0;
52 0   0       my $reason = $_[0]->{'reason'} || 'cross-wiki abuse';
53 0   0       my $expiry = $_[0]->{'expiry'} || '31 hours';
54 0 0         my $clobber = exists($_[0]->{'clobber'}) ? $_[0]->{'clobber'} : 1;
55              
56 0           my $start;
57 0 0         if ($ip =~ m/-/) {
    0          
58 0           $start = (split(/\-/, $ip, 2))[0];
59 0           $ip = range2cidr($start);
60             }
61             elsif ($ip =~ m,/\d\d$,) {
62 0           $start = $ip;
63 0           $start =~ s,/\d\d$,,;
64             }
65 0 0 0       unless ($ip =~ m,/\d\d$, || cidrvalidate($ip)) {
66 0 0         carp "Invalid IP $ip" if $self->{'debug'};
67             }
68              
69 0           my $opts = {
70             'wpAddress' => $ip, # mw-globalblock-address
71             'wpExpiryOther' => $expiry, # mw-globalblock-expiry-other
72             'wpReason' => $reason, # mw-globalblock-reason
73             'wpAnonOnly' => $ao, # mw-globalblock-anon-only
74             };
75 0           my $res = $self->_screenscrape_put('Special:GlobalBlock', $opts, 1);
76 0 0         if ($res->decoded_content() =~ m/class="error"/) {
77 0 0 0       if ($clobber and $res->decoded_content() =~ 'already blocked globally') {
78             # Resubmit unless noclobber
79 0           $res = $self->{'mech'}->submit_form(
80             with_fields => $opts,
81             );
82             }
83             else {
84 0           my $error = $self->_screenscrape_error($res->decoded_content());
85 0 0         carp $error if $self->{'debug'};
86 0           return;
87             }
88             }
89              
90 0           return $res;
91             }
92              
93              
94             sub g_unblock {
95 0     0 1   my $self = shift;
96 0 0         my $ip = ref $_[0] eq 'HASH' ? $_[0]->{'ip'} : shift;
97 0   0       my $reason = $_[0]->{'reason'} || 'Removing obsolete block';
98              
99 0           my $start;
100 0 0         if ($ip =~ m/-/) {
    0          
101 0           $start = (split(/\-/, $ip, 2))[0];
102 0           $ip = range2cidr($start);
103             }
104             elsif ($ip =~ m,/\d\d$,) {
105 0           $start = $ip;
106 0           $start =~ s,/\d\d$,,;
107             }
108 0 0 0       unless ($ip =~ m,/\d\d$, || cidrvalidate($ip)) {
109 0 0         carp "Invalid IP $ip" if $self->{'debug'};
110             }
111              
112 0 0         if ($start) {
113             # When rangeblocks are placed, the CIDR gets normalized - so you cannot unblock
114             # the same range you blocked. You'll need to do some kind of lookup. Probably,
115             # you can convert CIDR to A-B range, take the first IP, see whether it is blocked
116             # and what rangeblock affects it, then unblock that.
117              
118 0           $ip = $self->is_g_blocked($start);
119 0 0         unless ($ip) {
120 0 0         carp "Couldn't find the matching rangeblock" if $self->{'debug'};
121 0           return;
122             }
123             }
124              
125 0           my $opts = {
126             'address' => $ip,
127             'wpReason' => $reason,
128             };
129 0           my $res = $self->_screenscrape_put('Special:GlobalUnblock', $opts, 1);
130              
131 0 0         if ($res->decoded_content() =~ m/class="error"/) {
132 0           my $error = $self->_screenscrape_error($res->decoded_content());
133 0 0         carp $error if $self->{'debug'};
134 0           return;
135             }
136              
137 0           return 1;
138             }
139              
140              
141             sub ca_lock {
142 0     0 1   my $self = shift;
143 0 0         my $user = ref $_[0] eq 'HASH' ? $_[0]->{'user'} : shift;
144 0   0       my $hide = $_[0]->{'hide'} || 0;
145 0   0       my $reason = $_[0]->{'reason'} || 'cross-wiki abuse';
146 0 0         my $lock = defined($_[0]->{'lock'}) ? $_[0]->{'lock'} : 1;
147              
148 0 0         if ($hide == 0) {
    0          
    0          
149 0           $hide = '';
150             }
151             elsif ($hide == 1) {
152 0           $hide = 'lists';
153             }
154             elsif ($hide == 2) {
155 0           $hide = 'suppressed';
156             }
157 0           $user =~ s/^User://i;
158 0           $user =~ s/\@global$//i;
159              
160 0           my $res = $self->_screenscrape_put("Special:CentralAuth", {target=>$user}, 1);
161 0 0         if ($res->decoded_content() =~ m/class="error"/) {
162 0           my $error = $self->_screenscrape_error($res->decoded_content());
163 0 0         carp $error if $self->{'debug'};
164 0           return;
165             }
166              
167 0           $res = $self->{'mech'}->submit_form(
168             with_fields => {
169             wpStatusLocked => $lock,
170             wpStatusHidden => $hide,
171             wpReason => $reason,
172             },
173             );
174 0 0         if ($res->decoded_content() =~ m/class="error"/) {
175 0           my $error = $self->_screenscrape_error($res->decoded_content());
176 0 0         carp $error if $self->{'debug'};
177 0           return;
178             }
179              
180 0           return $res;
181             }
182              
183              
184             sub ca_unlock {
185 0     0 1   my $self = shift;
186 0 0         my $user = ref $_[0] eq 'HASH' ? $_[0]->{'user'} : shift;
187 0   0       my $hide = $_[0]->{'hide'} || 0;
188 0   0       my $reason = $_[0]->{'reason'} || 'Removing obsolete account lock';
189 0 0         my $lock = defined($_[0]->{'lock'}) ? $_[0]->{'lock'} : 0;
190              
191 0           return $self->ca_lock({
192             user => $user,
193             hide => $hide,
194             reason => $reason,
195             lock => $lock,
196             });
197             }
198              
199              
200             ################
201             # Internal use #
202             ################
203              
204             # Submits a form screenscrape-style (barf!)
205             sub _screenscrape_put {
206 0     0     my $self = shift;
207 0           my $page = shift;
208 0           my $options = shift;
209 0           my $no_esc = shift;
210 0           my $extra = shift;
211              
212 0           my $res = $self->_screenscrape_get($page, $no_esc, $extra);
213 0 0 0       return unless (ref($res) eq 'HTTP::Response' && $res->is_success);
214              
215 0           $res = $self->{'mech'}->submit_form(
216             with_fields => $options,
217             );
218              
219 0           return $res;
220             }
221              
222             # Gets a page screenscrape-style (barf!)
223             sub _screenscrape_get {
224 0     0     my $self = shift;
225 0           my $page = shift;
226 0   0       my $no_escape = shift || 0;
227 0   0       my $extra = shift || '&uselang=en&useskin=monobook';
228              
229 0 0         $page = uri_escape_utf8($page) unless $no_escape;
230              
231 0           my $url = "http://$self->{host}/$self->{path}/index.php?title=$page";
232 0 0         $url .= $extra if $extra;
233 0 0         print "Retrieving $url\n" if $self->{debug};
234              
235 0           my $res = $self->{'mech'}->get($url);
236 0 0 0       return unless (ref($res) eq 'HTTP::Response' && $res->is_success());
237              
238 0 0         if ($res->decoded_content() =~ m/class="error"/) {
239 0           my $error = $self->_screenscrape_error($res->decoded_content());
240 0 0         carp $error if $self->{'debug'};
241 0           return;
242             }
243              
244 0           return $res;
245             }
246              
247             # Returns the text of the first div with class 'error'
248             sub _screenscrape_error {
249 0     0     my $self = shift;
250 0           my $html = shift;
251              
252 0           require HTML::TreeBuilder;
253 0           my $tree = HTML::TreeBuilder->new_from_content($html);
254 0           my $error = $tree->look_down(
255             '_tag', 'div',
256             'class', 'error'
257             );
258              
259 0           my $error_text = $error->as_text();
260 0           $self->{'error'}->{'details'} = $error_text;
261 0           $self->{'error'}->{'code'} = 3;
262 0           return $error_text;
263             }
264              
265              
266             1;
267              
268              
269              
270             =pod
271              
272             =head1 NAME
273              
274             MediaWiki::Bot::Plugin::Steward - A plugin to MediaWiki::Bot providing steward functions
275              
276             =head1 VERSION
277              
278             version 0.0003
279              
280             =head1 SYNOPSIS
281              
282             use MediaWiki::Bot;
283             my $bot = MediaWiki::Bot->new({
284             operator => 'Mike.lifeguard',
285             assert => 'bot',
286             protocol => 'https',
287             host => 'secure.wikimedia.org',
288             path => 'wikipedia/meta/w',
289             login_data => { username => "Mike.lifeguard", password => $pass },
290             });
291             $bot->g_block({
292             ip => '127.0.0.1',
293             ao => 0,
294             summary => 'bloody vandals...',
295             });
296              
297             =head1 DESCRIPTION
298              
299             A plugin to the MediaWiki::Bot framework to provide steward functions to a bot.
300              
301             =head1 METHODS
302              
303             =head2 import()
304              
305             Calling import from any module will, quite simply, transfer these subroutines into that module's namespace. This is possible from any module which is compatible with MediaWiki::Bot.
306              
307             =head2 steward_new($data_hashref)
308              
309             =head2 g_block($data_hashref)
310              
311             This places a global block on an IP or IP range. You can provide either CIDR or classful ranges. To easily place a vandalism block, pass just the IP.
312              
313             =over 4
314              
315             =item *
316             ip - the IP or IP range to block. Use a single IP, CIDR range, or classful range.
317              
318             =item *
319             ao - whether to block anon-only; default is true.
320              
321             =item *
322             reason - the log summary. Default is 'cross-wiki abuse'.
323              
324             =item *
325             expiry - the expiry setting. Default is 31 hours.
326              
327             =back
328              
329             $bot->g_block({
330             ip => '127.0.0.1',
331             ao => 0,
332             reason => 'silly vandals',
333             expiry => '1 week',
334             });
335              
336             # Or, use defaults
337             $bot->g_block('127.0.0.0-127.0.0.255');
338              
339             =head2 g_unblock($data)
340              
341             Remove the global block affecting an IP or range. The hashref is:
342              
343             =over 4
344              
345             =item *
346             ip - the IP or range to unblock. You don't need to convert your range into a CIDR, just pass in your range in xxx.xxx.xxx.xxx-yyy.yyy.yyy.yyy format and let this method do the work.
347              
348             =item *
349             reason - the log reason. Default is 'Removing obsolete block'.
350              
351             =back
352              
353             If you pass only the IP, a generic reason will be used.
354              
355             $bot->g_unblock({
356             ip => '127.0.0.0-127.0.0.255',
357             reason => 'oops',
358             });
359             # Or
360             $bot->g_unblock('127.0.0.1');
361              
362             =head2 ca_lock($data)
363              
364             Locks and hides a user with CentralAuth. $data is a hash:
365              
366             =over 4
367              
368             =item *
369             user - the user to target
370              
371             =item *
372             lock - whether to lock or unlock the account - default is lock (0=unlocked, 1=locked)
373              
374             =item *
375             hide - how hard to hide the account - default is not at all (0=none, 1=lists, 2=oversight)
376              
377             =item *
378             reason - default is 'cross-wiki abuse'
379              
380             =back
381              
382             If you pass in only a username, the account will be locked but not hidden, and the default reason will be used:
383              
384             $bot->ca_lock("Mike.lifeguard");
385             # Or, the more complete call:
386             $bot->ca_lock({
387             user => "Mike.lifeguard",
388             reason => "test",
389             });
390              
391             =head2 ca_unlock($data)
392              
393             Same parameters as ca_lock(), but with the default setting for lock reversed (ie, default is I).
394              
395             =head1 AUTHORS
396              
397             =over 4
398              
399             =item *
400              
401             Mike.lifeguard
402              
403             =item *
404              
405             patch and bug report contributors
406              
407             =back
408              
409             =head1 COPYRIGHT AND LICENSE
410              
411             This software is Copyright (c) 2010 by the MediaWiki::Bot team .
412              
413             This is free software, licensed under:
414              
415             The GNU General Public License, Version 3, June 2007
416              
417             =cut
418              
419              
420             __END__