File Coverage

blib/lib/MediaWiki/Bot.pm
Criterion Covered Total %
statement 776 1103 70.3
branch 294 602 48.8
condition 55 169 32.5
subroutine 62 78 79.4
pod 58 58 100.0
total 1245 2010 61.9


line stmt bran cond sub pod time code
1             package MediaWiki::Bot;
2 44     44   1926581 use strict;
  44         59  
  44         1127  
3 44     44   149 use warnings;
  44         47  
  44         1675  
4             # ABSTRACT: a high-level bot framework for interacting with MediaWiki wikis
5             our $VERSION = '5.006003'; # VERSION
6              
7 44     44   19433 use HTML::Entities 3.28;
  44         175910  
  44         3027  
8 44     44   221 use Carp;
  44         45  
  44         2122  
9 44     44   157 use Digest::MD5 2.39 qw(md5_hex);
  44         633  
  44         1885  
10 44     44   21715 use Encode qw(encode_utf8);
  44         327068  
  44         2925  
11 44     44   21496 use MediaWiki::API 0.36;
  44         1960181  
  44         1335  
12 44     44   315 use List::Util qw(sum);
  44         60  
  44         3797  
13 44     44   17599 use MediaWiki::Bot::Constants qw(:all);
  44         92  
  44         7891  
14              
15 44     44   204 use Exporter qw(import);
  44         52  
  44         2571  
16             our @EXPORT_OK = @{ $MediaWiki::Bot::Constants::EXPORT_TAGS{all} };
17             our %EXPORT_TAGS = ( constants => \@EXPORT_OK );
18              
19 44     44   18613 use Module::Pluggable search_path => [qw(MediaWiki::Bot::Plugin)], 'require' => 1;
  44         356216  
  44         261  
20             foreach my $plugin (__PACKAGE__->plugins) {
21             #print "Found plugin $plugin\n";
22             $plugin->import();
23             }
24              
25              
26             sub new {
27 43     43 1 7605 my $package = shift;
28 43         66 my $agent;
29             my $assert;
30 0         0 my $operator;
31 0         0 my $maxlag;
32 0         0 my $protocol;
33 0         0 my $host;
34 0         0 my $path;
35 0         0 my $login_data;
36 0         0 my $debug;
37              
38 43 50       159 if (ref $_[0] eq 'HASH') {
39 43         80 $agent = $_[0]->{agent};
40 43         65 $assert = $_[0]->{assert};
41 43         55 $operator = $_[0]->{operator};
42 43         58 $maxlag = $_[0]->{maxlag};
43 43         57 $protocol = $_[0]->{protocol};
44 43         51 $host = $_[0]->{host};
45 43         60 $path = $_[0]->{path};
46 43         49 $login_data = $_[0]->{login_data};
47 43         60 $debug = $_[0]->{debug};
48             }
49             else {
50 0 0       0 warnings::warnif('deprecated', 'Please pass a hashref; this method of calling '
51             . 'the constructor is deprecated and will be removed in a future release')
52             if @_;
53 0         0 $agent = shift;
54 0         0 $assert = shift;
55 0         0 $operator = shift;
56 0         0 $maxlag = shift;
57 0         0 $protocol = shift;
58 0         0 $host = shift;
59 0         0 $path = shift;
60 0         0 $debug = shift;
61             }
62              
63 43 100       109 $assert =~ s/[&?]assert=// if $assert; # Strip out param part, leaving just the value
64 43 100       97 $operator =~ s/^User://i if $operator;
65              
66 43 50 66     340 if (not $agent and not $operator) {
    100 66        
67 0         0 carp q{You should provide either a customized user agent string }
68             . q{(see https://meta.wikimedia.org/wiki/User-agent_policy) }
69             . q{or provide your username as `operator'.};
70             }
71             elsif (not $agent and $operator) {
72 3         5 $operator =~ s{^User:}{};
73 3 50       41 $agent = sprintf(
74             'Perl MediaWiki::Bot/%s (%s; [[User:%s]])',
75             (defined __PACKAGE__->VERSION ? __PACKAGE__->VERSION : 'dev'),
76             'https://metacpan.org/MediaWiki::Bot',
77             $operator
78             );
79             }
80              
81 43         89 my $self = bless({}, $package);
82 43         199 $self->{errstr} = '';
83 43 100       104 $self->{assert} = $assert if $assert;
84 43         67 $self->{operator} = $operator;
85 43   50     222 $self->{debug} = $debug || 0;
86 43 50       422 $self->{api} = MediaWiki::API->new({
87             max_lag => (defined $maxlag ? $maxlag : 5),
88             max_lag_delay => 5,
89             max_lag_retries => 5,
90             retries => 5,
91             retry_delay => 10, # no infinite loops
92             use_http_get => 1, # use HTTP GET to make certain requests cacheable
93             });
94 43 50       406730 $self->{api}->{ua}->agent($agent) if defined $agent;
95              
96             # Set wiki (handles setting $self->{host} etc)
97 43         2058 $self->set_wiki({
98             protocol => $protocol,
99             host => $host,
100             path => $path,
101             });
102              
103             # Log-in, and maybe autoconfigure
104 43 50       138 if ($login_data) {
105 0         0 my $success = $self->login($login_data);
106 0 0       0 if ($success) {
107 0         0 return $self;
108             }
109             else {
110 0 0       0 carp "Couldn't log in with supplied settings" if $self->{debug};
111 0         0 return;
112             }
113             }
114              
115 43         134 return $self;
116             }
117              
118              
119             sub set_wiki {
120 43     43 1 72 my $self = shift;
121 43         66 my $host;
122             my $path;
123 0         0 my $protocol;
124              
125 43 50       166 if (ref $_[0] eq 'HASH') {
126 43         84 $host = $_[0]->{host};
127 43         73 $path = $_[0]->{path};
128 43         76 $protocol = $_[0]->{protocol};
129             }
130             else {
131 0         0 warnings::warnif('deprecated', 'Please pass a hashref; this method of calling '
132             . 'set_wiki is deprecated, and will be removed in a future release');
133 0         0 $host = shift;
134 0         0 $path = shift;
135             }
136              
137             # Set defaults
138 43 50 50     330 $protocol = $self->{protocol} || 'https' unless defined($protocol);
139 43 100 50     157 $host = $self->{host} || 'en.wikipedia.org' unless defined($host);
140 43 100 50     349 $path = $self->{path} || 'w' unless defined($path);
141              
142             # Clean up the parts we will build a URL with
143 43         105 $protocol =~ s,://$,,;
144 43 50 33     201 if ($host =~ m,^(http|https)(://)?, && !$protocol) {
145 0         0 $protocol = $1;
146             }
147 43         74 $host =~ s,^https?://,,;
148 43         79 $host =~ s,/$,,;
149 43         76 $path =~ s,/$,,;
150              
151             # Invalidate wiki-specific cached data
152 43 50 33     584 if ( ((defined($self->{host})) and ($self->{host} ne $host))
      33        
      33        
      33        
      33        
153             or ((defined($self->{path})) and ($self->{path} ne $path))
154             or ((defined($self->{protocol})) and ($self->{protocol} ne $protocol))
155             ) {
156 0 0       0 delete $self->{ns_data} if $self->{ns_data};
157 0 0       0 delete $self->{ns_alias_data} if $self->{ns_alias_data};
158             }
159              
160 43         96 $self->{protocol} = $protocol;
161 43         73 $self->{host} = $host;
162 43         65 $self->{path} = $path;
163              
164 43 100       281 $self->{api}->{config}->{api_url} = $path
165             ? "$protocol://$host/$path/api.php"
166             : "$protocol://$host/api.php"; # $path is '', so don't use http://domain.com//api.php
167 43 50       131 warn "Wiki set to " . $self->{api}->{config}{api_url} . "\n" if $self->{debug} > 1;
168              
169 43         73 return RET_TRUE;
170             }
171              
172              
173             sub login {
174 1     1 1 8 my $self = shift;
175 1         2 my $username;
176             my $password;
177 0         0 my $lgdomain;
178 0         0 my $autoconfig;
179 0         0 my $basic_auth;
180 0         0 my $do_sul;
181 1 50       32 if (ref $_[0] eq 'HASH') {
182 1         2 $username = $_[0]->{username};
183 1         2 $password = $_[0]->{password};
184 1 50       4 $autoconfig = defined($_[0]->{autoconfig}) ? $_[0]->{autoconfig} : 1;
185 1         2 $basic_auth = $_[0]->{basic_auth};
186 1   50     5 $do_sul = $_[0]->{do_sul} || 0;
187 1         2 $lgdomain = $_[0]->{lgdomain};
188             }
189             else {
190 0         0 warnings::warnif('deprecated', 'Please pass a hashref; this method of calling '
191             . 'login is deprecated and will be removed in a future release');
192 0         0 $username = shift;
193 0         0 $password = shift;
194 0         0 $autoconfig = 0;
195 0         0 $do_sul = 0;
196             }
197              
198             # strip off the "@bot_password_label" suffix, if any
199 1         5 $self->{username} = (split /@/, $username, 2)[0]; # normal human-readable username
200 1         2 $self->{login_username} = $username; # to be used for login (includes "@bot_password_label")
201              
202             carp "Logging in over plain HTTP is a bad idea, we would be sending secrets"
203             . " (passwords or cookies) in plaintext over an insecure connection."
204             . " To protect against eavesdroppers, set protocol => 'https'"
205 1 50       3 unless $self->{protocol} eq 'https';
206              
207             # Handle basic auth first, if needed
208 1 50       3 if ($basic_auth) {
209 0 0       0 warn 'Applying basic auth credentials' if $self->{debug} > 1;
210             $self->{api}->{ua}->credentials(
211             $basic_auth->{netloc},
212             $basic_auth->{realm},
213             $basic_auth->{uname},
214             $basic_auth->{pass}
215 0         0 );
216             }
217              
218 1 50       5 if ($self->{host} eq 'secure.wikimedia.org') {
219 0         0 warnings::warnif('deprecated', 'SSL is now supported on the main Wikimedia Foundation sites. '
220             . 'Use en.wikipedia.org (or whatever) instead of secure.wikimedia.org.');
221 0         0 return;
222             }
223              
224 1 50       7 if($do_sul) {
225 0         0 my $sul_success = $self->_do_sul($password);
226 0 0 0     0 warn 'Some or all SUL logins failed' if $self->{debug} > 1 and !$sul_success;
227             }
228              
229 1         3 my $cookies = ".mediawiki-bot-$username-cookies";
230 1 50       28 if (-r $cookies) {
231 0         0 $self->{api}->{ua}->{cookie_jar}->load($cookies);
232 0         0 $self->{api}->{ua}->{cookie_jar}->{ignore_discard} = 1;
233             # $self->{api}->{ua}->add_handler("request_send", sub { shift->dump; return });
234              
235 0 0       0 if ($self->_is_loggedin()) {
236 0 0       0 $self->_do_autoconfig() if $autoconfig;
237 0 0       0 warn 'Logged in successfully with cookies' if $self->{debug} > 1;
238 0         0 return 1; # If we're already logged in, nothing more is needed
239             }
240             }
241              
242 1 50       2 unless ($password) {
243 0 0       0 carp q{Cookies didn't get us logged in, and no password to continue with authentication} if $self->{debug};
244 0         0 return;
245             }
246              
247 1         1 my $res;
248 1         4 RETRY: for (1..2) {
249             # Fetch a login token
250             $res = $self->{api}->api({
251 2 50       19 action => 'query',
252             meta => 'tokens',
253             type => 'login',
254             }) or return $self->_handle_api_error();
255 2         637846 my $token = $res->{query}->{tokens}->{logintoken};
256              
257             # Do the login
258             $res = $self->{api}->api({
259             action => 'login',
260             lgname => $self->{login_username},
261 2 50       23 lgpassword => $password,
262             lgdomain => $lgdomain,
263             lgtoken => $token,
264             }) or return $self->_handle_api_error();
265              
266 2 50       3423250 last RETRY if $res->{login}->{result} eq 'Success';
267             };
268              
269 1         6 $self->{api}->{ua}->{cookie_jar}->extract_cookies($self->{api}->{response});
270 1 50 33     69 $self->{api}->{ua}->{cookie_jar}->save($cookies) if (-w($cookies) or -w('.'));
271              
272 1 50       286 if ($res->{login}->{result} eq 'Success') {
273 0 0       0 if ($res->{login}->{lgusername} eq $self->{username}) {
274 0 0       0 $self->_do_autoconfig() if $autoconfig;
275 0 0       0 warn 'Logged in successfully with password' if $self->{debug} > 1;
276             }
277             }
278              
279             return ((defined($res->{login}->{lgusername})) and
280             (defined($res->{login}->{result})) and
281             ($res->{login}->{lgusername} eq $self->{username}) and
282 1   0     9 ($res->{login}->{result} eq 'Success'));
283             }
284              
285             sub _do_sul {
286 0     0   0 my $self = shift;
287 0         0 my $password = shift;
288 0         0 my $debug = $self->{debug}; # Remember these for later
289 0         0 my $host = $self->{host};
290 0         0 my $path = $self->{path};
291 0         0 my $protocol = $self->{protocol};
292 0         0 my $username = $self->{login_username};
293              
294 0         0 $self->{debug} = 0; # Turn off debugging for these internal calls
295 0         0 my @logins; # Keep track of our successes
296 0         0 my @WMF_projects = qw(
297             en.wikipedia.org
298             en.wiktionary.org
299             en.wikibooks.org
300             en.wikinews.org
301             en.wikiquote.org
302             en.wikisource.org
303             en.wikiversity.org
304             meta.wikimedia.org
305             commons.wikimedia.org
306             species.wikimedia.org
307             incubator.wikimedia.org
308             );
309              
310 0         0 SUL: foreach my $project (@WMF_projects) { # Could maybe be parallelized
311 0 0       0 print STDERR "Logging in on $project..." if $debug > 1;
312 0         0 $self->set_wiki({
313             host => $project,
314             });
315 0         0 my $success = $self->login({
316             username => $username,
317             password => $password,
318             do_sul => 0,
319             autoconfig => 0,
320             });
321 0 0       0 warn ($success ? " OK\n" : " FAILED:\n") if $debug > 1;
    0          
322             warn $self->{api}->{error}->{code} . ': ' . $self->{api}->{error}->{details}
323 0 0 0     0 if $debug > 1 and !$success;
324 0         0 push(@logins, $success);
325             }
326             $self->set_wiki({ # Switch back to original wiki
327 0         0 protocol => $protocol,
328             host => $host,
329             path => $path,
330             });
331              
332 0         0 my $sum = sum 0, @logins;
333 0         0 my $total = scalar @WMF_projects;
334 0 0       0 warn "$sum/$total logins succeeded" if $debug > 1;
335 0         0 $self->{debug} = $debug; # Reset debug to it's old value
336              
337 0         0 return $sum == $total;
338             }
339              
340              
341             sub logout {
342 0     0 1 0 my $self = shift;
343              
344 0         0 $self->{api}->api({ action => 'logout' });
345 0         0 return RET_TRUE;
346             }
347              
348              
349             sub edit {
350 1     1 1 30 my $self = shift;
351 1         1 my $page;
352             my $text;
353 0         0 my $summary;
354 0         0 my $is_minor;
355 0         0 my $assert;
356 0         0 my $markasbot;
357 0         0 my $section;
358 0         0 my $captcha_id;
359 0         0 my $captcha_solution;
360              
361 1 50       3 if (ref $_[0] eq 'HASH') {
362 1         1 $page = $_[0]->{page};
363 1         2 $text = $_[0]->{text};
364 1         1 $summary = $_[0]->{summary};
365 1         1 $is_minor = $_[0]->{minor};
366 1         2 $assert = $_[0]->{assert};
367 1         1 $markasbot = $_[0]->{markasbot};
368 1         1 $section = $_[0]->{section};
369 1         2 $captcha_id = $_[0]->{captcha_id};
370 1         5 $captcha_solution = $_[0]->{captcha_solution};
371             }
372             else {
373 0         0 warnings::warnif('deprecated', 'Please pass a hashref; this method of calling '
374             . 'edit is deprecated, and will be removed in a future release.');
375 0         0 $page = shift;
376 0         0 $text = shift;
377 0         0 $summary = shift;
378 0         0 $is_minor = shift;
379 0         0 $assert = shift;
380 0         0 $markasbot = shift;
381 0         0 $section = shift;
382             }
383              
384             # Set defaults
385 1 50       2 $summary = 'BOT: Changing page text' unless $summary;
386 1 50       3 if ($assert) {
387 1         2 $assert =~ s/^[&?]assert=//;
388             }
389             else {
390 0         0 $assert = $self->{assert};
391             }
392 1 50       2 $is_minor = 1 unless defined($is_minor);
393 1 50       2 $markasbot = 1 unless defined($markasbot);
394              
395             # Clear any captcha data that might remain from a previous edit attempt
396 1         2 delete $self->{error}->{captcha};
397 1 50 33     6 carp 'Need both captcha_id and captcha_solution when editing with a solved CAPTCHA'
      33        
      33        
398             if (defined $captcha_id and not defined $captcha_solution)
399             or (defined $captcha_solution and not defined $captcha_id);
400              
401 1         4 my ($edittoken, $lastedit, $tokentime) = $self->_get_edittoken($page);
402 1 50       4 return $self->_handle_api_error() unless $edittoken;
403              
404             # HTTP::Message will do this eventually as of 6.03 (RT#75592), so we need
405             # to do it here - otherwise, the md5 won't match what eventually is sent to
406             # the server, and the edit will fail - GH#39.
407             # If HTTP::Message becomes unbroken in the future, might have to keep this
408             # workaround for people using 6.03 and other future broken versions.
409 1         10 $text =~ s{(?
410 1         5 my $md5 = md5_hex(encode_utf8($text)); # Pass only bytes to md5_hex()
411 1 50       26 my $hash = {
    50          
    50          
    50          
    50          
412             action => 'edit',
413             title => $page,
414             token => $edittoken,
415             text => $text,
416             md5 => $md5, # Guard against data corruption
417             summary => $summary,
418             basetimestamp => $lastedit, # Guard against edit conflicts
419             starttimestamp => $tokentime, # Guard against the page being deleted/moved
420             bot => $markasbot,
421             ( $section ? (section => $section) : ()),
422             ( $assert ? (assert => $assert) : ()),
423             ( $is_minor ? (minor => 1) : (notminor => 1)),
424             ( $captcha_id ? (captchaid => $captcha_id) : ()),
425             ( $captcha_solution ? (captchaword => $captcha_solution) : ()),
426             };
427              
428             ### Actually do the edit
429 1         4 my $res = $self->{api}->api($hash);
430 1 50       213414 return $self->_handle_api_error() unless $res;
431              
432 0 0 0     0 if ($res->{edit}->{result} && $res->{edit}->{result} eq 'Failure') {
433             # https://www.mediawiki.org/wiki/API:Edit#CAPTCHAs_and_extension_errors
434             # You need to solve the CAPTCHA, then retry the request with the ID in
435             # this error response and the solution.
436 0 0       0 if (exists $res->{edit}->{captcha}) {
437             return $self->_handle_api_error({
438             code => ERR_CAPTCHA,
439             details => 'captcharequired: This action requires that a CAPTCHA be solved',
440             captcha => $res->{edit}->{captcha},
441 0         0 });
442             }
443 0         0 return $self->_handle_api_error();
444             }
445              
446 0         0 return $res;
447             }
448              
449              
450             sub move {
451 0     0 1 0 my $self = shift;
452 0         0 my $from = shift;
453 0         0 my $to = shift;
454 0         0 my $reason = shift;
455 0         0 my $opts = shift;
456              
457 0         0 my $hash = {
458             action => 'move',
459             from => $from,
460             to => $to,
461             reason => $reason,
462             };
463 0 0       0 $hash->{movetalk} = $opts->{movetalk} if defined($opts->{movetalk});
464 0 0       0 $hash->{noredirect} = $opts->{noredirect} if defined($opts->{noredirect});
465 0 0       0 $hash->{movesubpages} = $opts->{movesubpages} if defined($opts->{movesubpages});
466              
467 0         0 my $res = $self->{api}->edit($hash);
468 0 0       0 return $self->_handle_api_error() unless $res;
469 0         0 return $res; # should we return something more useful?
470             }
471              
472              
473             sub get_history {
474 2     2 1 14 my $self = shift;
475 2         4 my $pagename = shift;
476 2   50     5 my $limit = shift || 'max';
477 2         2 my $rvstartid = shift;
478 2         4 my $direction = shift;
479              
480 2         10 my $hash = {
481             action => 'query',
482             prop => 'revisions',
483             titles => $pagename,
484             rvprop => 'ids|timestamp|user|comment|flags',
485             rvlimit => $limit
486             };
487              
488 2 50       4 $hash->{rvstartid} = $rvstartid if ($rvstartid);
489 2 50       6 $hash->{direction} = $direction if ($direction);
490              
491 2         9 my $res = $self->{api}->api($hash);
492 2 50       816529 return $self->_handle_api_error() unless $res;
493 2         4 my ($id) = keys %{ $res->{query}->{pages} };
  2         9  
494 2         5 my $array = $res->{query}->{pages}->{$id}->{revisions};
495              
496 2         3 my @return;
497 2         2 foreach my $hash (@{$array}) {
  2         6  
498 7         6 my $revid = $hash->{revid};
499 7         9 my $user = $hash->{user};
500 7         13 my ($timestamp_date, $timestamp_time) = split(/T/, $hash->{timestamp});
501 7         18 $timestamp_time =~ s/Z$//;
502 7         9 my $comment = $hash->{comment};
503             push(
504             @return,
505             {
506             revid => $revid,
507             user => $user,
508             timestamp_date => $timestamp_date,
509             timestamp_time => $timestamp_time,
510             comment => $comment,
511             minor => exists $hash->{minor},
512 7         28 });
513             }
514 2         25 return @return;
515             }
516              
517              
518             sub get_text {
519 10     10 1 3676 my $self = shift;
520 10         19 my $pagename = shift;
521 10         14 my $revid = shift;
522 10         12 my $section = shift;
523              
524 10         52 my $hash = {
525             action => 'query',
526             titles => $pagename,
527             prop => 'revisions',
528             rvprop => 'content',
529             };
530 10 50       33 $hash->{rvstartid} = $revid if ($revid);
531 10 100       28 $hash->{rvsection} = $section if ($section);
532              
533 10         47 my $res = $self->{api}->api($hash);
534 10 50       2457208 return $self->_handle_api_error() unless $res;
535 10         18 my ($id, $data) = %{ $res->{query}->{pages} };
  10         46  
536              
537 10 100       59 return if $id == PAGE_NONEXISTENT;
538 8         81 return $data->{revisions}[0]->{'*'}; # the wikitext
539             }
540              
541              
542             sub get_id {
543 1     1 1 5 my $self = shift;
544 1         2 my $pagename = shift;
545              
546 1         2 my $hash = {
547             action => 'query',
548             titles => $pagename,
549             };
550              
551 1         3 my $res = $self->{api}->api($hash);
552 1 50       391954 return $self->_handle_api_error() unless $res;
553 1         2 my ($id) = %{ $res->{query}->{pages} };
  1         5  
554 1 50       5 return if $id == PAGE_NONEXISTENT;
555 1         9 return $id;
556             }
557              
558              
559             sub get_pages {
560 2     2 1 2793 my $self = shift;
561 2 100       9 my @pages = (ref $_[0] eq 'ARRAY') ? @{$_[0]} : @_;
  1         3  
562 2         2 my %return;
563              
564 2         10 my $hash = {
565             action => 'query',
566             titles => join('|', @pages),
567             prop => 'revisions',
568             rvprop => 'content',
569             };
570              
571 2         2 my $diff; # Used to track problematic article names
572 2         3 map { $diff->{$_} = 1; } @pages;
  8         12  
573              
574 2         8 my $res = $self->{api}->api($hash);
575 2 50       699904 return $self->_handle_api_error() unless $res;
576              
577 2         3 foreach my $id (keys %{ $res->{query}->{pages} }) {
  2         11  
578 8         9 my $page = $res->{query}->{pages}->{$id};
579 8 100       22 if ($diff->{ $page->{title} }) {
580 6         7 $diff->{ $page->{title} }++;
581             }
582             else {
583 2         4 next;
584             }
585              
586 6 100       10 if (defined($page->{missing})) {
587 2         6 $return{ $page->{title} } = undef;
588 2         3 next;
589             }
590 4 50       9 if (defined($page->{revisions})) {
591 4         6 my $revisions = @{ $page->{revisions} }[0]->{'*'};
  4         7  
592 4 50 33     13 if (!defined $revisions) {
    50          
593 0         0 $return{ $page->{title} } = $revisions;
594             }
595             elsif (length($revisions) < 150 && $revisions =~ m/\#REDIRECT\s\[\[([^\[\]]+)\]\]/) { # FRAGILE!
596 0         0 my $redirect_to = $1;
597 0         0 $return{ $page->{title} } = $self->get_text($redirect_to);
598             }
599             else {
600 4         9 $return{ $page->{title} } = $revisions;
601             }
602             }
603             }
604              
605 2         9 my $expand = $self->_get_ns_alias_data();
606             # Only for those article names that remained after the first part
607             # If we're here we are dealing most likely with a WP:CSD type of article name
608 2         8 for my $title (keys %$diff) {
609 8 100       20 if ($diff->{$title} == 1) {
610 2         6 my @pieces = split(/:/, $title);
611 2 50       5 if (@pieces > 1) {
612 2   33     6 $pieces[0] = ($expand->{ $pieces[0] } || $pieces[0]);
613 2         13 my $v = $self->get_text(join ':', @pieces);
614 2 50       7 warn "Detected article name that needed expanding $title\n" if $self->{debug} > 1;
615              
616 2         5 $return{$title} = $v;
617 2 50 33     22 if (defined $v and $v =~ m/\#REDIRECT\s\[\[([^\[\]]+)\]\]/) {
618 0         0 $v = $self->get_text($1);
619 0         0 $return{$title} = $v;
620             }
621             }
622             }
623             }
624 2         35 return \%return;
625             }
626              
627              
628             sub get_image{
629 0     0 1 0 my $self = shift;
630 0         0 my $name = shift;
631 0         0 my $options = shift;
632              
633 0         0 my %sizeparams;
634 0 0       0 $sizeparams{iiurlwidth} = $options->{width} if $options->{width};
635 0 0       0 $sizeparams{iiurlheight} = $options->{height} if $options->{height};
636              
637             my $ref = $self->{api}->api({
638 0         0 action => 'query',
639             titles => $name,
640             prop => 'imageinfo',
641             iiprop => 'url|size',
642             %sizeparams
643             });
644 0 0       0 return $self->_handle_api_error() unless $ref;
645 0         0 my ($pageref) = values %{ $ref->{query}->{pages} };
  0         0  
646 0 0       0 return unless defined $pageref->{imageinfo}; # if the image is missing
647              
648 0   0     0 my $url = @{ $pageref->{imageinfo} }[0]->{thumburl} || @{ $pageref->{imageinfo} }[0]->{url};
649 0 0       0 die "$url should be absolute or something." unless ( $url =~ m{^https?://} );
650              
651 0         0 my $response = $self->{api}->{ua}->get($url);
652 0 0       0 return $self->_handle_api_error() unless ( $response->code == 200 );
653 0         0 return $response->decoded_content;
654             }
655              
656              
657             sub revert {
658 0     0 1 0 my $self = shift;
659 0         0 my $pagename = shift;
660 0         0 my $revid = shift;
661 0   0     0 my $summary = shift || "Reverting to old revision $revid";
662              
663 0         0 my $text = $self->get_text($pagename, $revid);
664 0         0 my $res = $self->edit({
665             page => $pagename,
666             text => $text,
667             summary => $summary,
668             });
669              
670 0         0 return $res;
671             }
672              
673              
674             sub undo {
675 0     0 1 0 my $self = shift;
676 0         0 my $page = shift;
677 0   0     0 my $revid = shift || croak "No revid given";
678 0   0     0 my $summary = shift || "Reverting revision #$revid";
679 0         0 my $after = shift;
680 0 0       0 $summary = "Reverting edits between #$revid & #$after" if defined($after); # Is that clear? Correct?
681              
682 0         0 my ($edittoken, $basetimestamp, $starttimestamp) = $self->_get_edittoken($page);
683 0         0 my $hash = {
684             action => 'edit',
685             title => $page,
686             undo => $revid,
687             (undoafter => $after)x!! defined $after,
688             summary => $summary,
689             token => $edittoken,
690             starttimestamp => $starttimestamp,
691             basetimestamp => $basetimestamp,
692             };
693              
694 0         0 my $res = $self->{api}->api($hash);
695 0 0       0 return $self->_handle_api_error() unless $res;
696 0         0 return $res;
697             }
698              
699              
700             sub get_last {
701 3     3 1 15 my $self = shift;
702 3         4 my $page = shift;
703 3         3 my $user = shift;
704              
705             my $res = $self->{api}->api({
706 3   100     32 action => 'query',
707             titles => $page,
708             prop => 'revisions',
709             rvlimit => 1,
710             rvprop => 'ids|user',
711             rvexcludeuser => $user || '',
712             });
713 3 100       1167914 return $self->_handle_api_error() unless $res;
714              
715 2         4 my (undef, $data) = %{ $res->{query}->{pages} };
  2         8  
716 2         4 my $revid = $data->{revisions}[0]->{revid};
717 2         16 return $revid;
718             }
719              
720              
721             sub update_rc {
722 1     1 1 74 warnings::warnif('deprecated', 'update_rc is deprecated, and may be removed '
723             . 'in a future release. Please use recentchanges(), which provides more '
724             . 'data, including rcid');
725 1         655 my $self = shift;
726 1   50     3 my $limit = shift || 'max';
727 1         2 my $options = shift;
728              
729 1         4 my $hash = {
730             action => 'query',
731             list => 'recentchanges',
732             rcnamespace => 0,
733             rclimit => $limit,
734             };
735 1 50       4 $options->{max} = 1 unless $options->{max};
736              
737 1         6 my $res = $self->{api}->list($hash, $options);
738 1 50       390331 return $self->_handle_api_error() unless $res;
739 1 50       4 return RET_TRUE if not ref $res; # Not a ref when using callback
740              
741 1         1 my @rc_table;
742 1         1 foreach my $hash (@{$res}) {
  1         3  
743             push(
744             @rc_table,
745             {
746             title => $hash->{title},
747             revid => $hash->{revid},
748             old_revid => $hash->{old_revid},
749             timestamp => $hash->{timestamp},
750             }
751 2         7 );
752             }
753 1         11 return @rc_table;
754             }
755              
756              
757             sub recentchanges {
758 5     5 1 63274 my $self = shift;
759 5         10 my $ns;
760             my $limit;
761 0         0 my $options;
762 0         0 my $user;
763 0         0 my $show;
764 5 100       21 if (ref $_[0] eq 'HASH') { # unpack for new args
765 2         5 my %args = %{ +shift };
  2         11  
766 2         5 $ns = delete $args{ns};
767 2         9 $limit = delete $args{limit};
768 2         6 $user = delete $args{user};
769              
770 2 50       10 if (ref $args{show} eq 'HASH') {
771 0         0 my @show;
772 0         0 while (my ($k, $v) = each %{ $args{show} }) {
  0         0  
773 0         0 push @show, '!'x!$v . $k;
774             }
775 0         0 $show = join '|', @show;
776             }
777             else {
778 2         3 $show = delete $args{show};
779             }
780              
781 2         3 $options = shift;
782             }
783             else {
784 3   100     13 $ns = shift || 0;
785 3   100     10 $limit = shift || 50;
786 3         6 $options = shift;
787             }
788 5 100       20 $ns = join('|', @$ns) if ref $ns eq 'ARRAY';
789              
790 5         27 my $hash = {
791             action => 'query',
792             list => 'recentchanges',
793             rcnamespace => $ns,
794             rclimit => $limit,
795             rcprop => 'user|comment|timestamp|title|ids',
796             };
797 5 50       16 $hash->{rcuser} = $user if defined $user;
798 5 50       11 $hash->{rcshow} = $show if defined $show;
799              
800 5 50       21 $options->{max} = 1 unless $options->{max};
801              
802 5 50       39 my $res = $self->{api}->list($hash, $options)
803             or return $self->_handle_api_error();
804 5 100       1323612 return RET_TRUE unless ref $res; # Not a ref when using callback
805 3         47 return @$res;
806             }
807              
808              
809             sub what_links_here {
810 2     2 1 3067 my $self = shift;
811 2         3 my $page = shift;
812 2         1 my $filter = shift;
813 2         4 my $ns = shift;
814 2         2 my $options = shift;
815              
816 2 50       6 $ns = join('|', @$ns) if (ref $ns eq 'ARRAY'); # Allow array of namespaces
817 2 50 33     22 if (defined($filter) and $filter =~ m/(all|redirects|nonredirects)/) { # Verify $filter
818 2         6 $filter = $1;
819             }
820              
821             # http://en.wikipedia.org/w/api.php?action=query&list=backlinks&bltitle=template:tlx
822 2         8 my $hash = {
823             action => 'query',
824             list => 'backlinks',
825             bltitle => $page,
826             bllimit => 'max',
827             };
828 2 100       6 $hash->{blnamespace} = $ns if defined $ns;
829 2 50       5 $hash->{blfilterredir} = $filter if $filter;
830 2 50       5 $options->{max} = 1 unless $options->{max};
831              
832 2         9 my $res = $self->{api}->list($hash, $options);
833 2 50       618724 return $self->_handle_api_error() unless $res;
834 2 100       13 return RET_TRUE if not ref $res; # When using a callback hook, this won't be a reference
835 1         3 my @links;
836 1         3 foreach my $hashref (@$res) {
837 21         22 my $title = $hashref->{title};
838 21         19 my $redirect = defined($hashref->{redirect});
839 21         59 push @links, { title => $title, redirect => $redirect };
840             }
841              
842 1         51 return @links;
843             }
844              
845              
846             sub list_transclusions {
847 2     2 1 2423 my $self = shift;
848 2         4 my $page = shift;
849 2         3 my $filter = shift;
850 2         2 my $ns = shift;
851 2         3 my $options = shift;
852              
853 2 50       5 $ns = join('|', @$ns) if (ref $ns eq 'ARRAY');
854 2 50 33     24 if (defined($filter) and $filter =~ m/(all|redirects|nonredirects)/) { # Verify $filter
855 2         4 $filter = $1;
856             }
857              
858             # http://en.wikipedia.org/w/api.php?action=query&list=embeddedin&eititle=Template:Stub
859 2         10 my $hash = {
860             action => 'query',
861             list => 'embeddedin',
862             eititle => $page,
863             eilimit => 'max',
864             };
865 2 50       7 $hash->{eifilterredir} = $filter if $filter;
866 2 50       4 $hash->{einamespace} = $ns if defined $ns;
867 2 50       6 $options->{max} = 1 unless $options->{max};
868              
869 2         14 my $res = $self->{api}->list($hash, $options);
870 2 50       595276 return $self->_handle_api_error() unless $res;
871 2 100       14 return RET_TRUE if not ref $res; # When using a callback hook, this won't be a reference
872 1         2 my @links;
873 1         3 foreach my $hashref (@$res) {
874 176         120 my $title = $hashref->{title};
875 176         111 my $redirect = defined($hashref->{redirect});
876 176         222 push @links, { title => $title, redirect => $redirect };
877             }
878              
879 1         64 return @links;
880             }
881              
882              
883             sub get_pages_in_category {
884 14     14 1 19 my $self = shift;
885 14         17 my $category = shift;
886 14         17 my $options = shift;
887              
888 14 50       44 if ($category =~ m/:/) { # It might have a namespace name
889 14         55 my ($cat) = split(/:/, $category, 2);
890 14 50       51 if ($cat ne 'Category') { # 'Category' is a canonical name for ns14
891 0         0 my $ns_data = $self->_get_ns_data();
892 0         0 my $cat_ns_name = $ns_data->{+NS_CATEGORY};
893 0 0       0 if ($cat ne $cat_ns_name) {
894 0         0 $category = "$cat_ns_name:$category";
895             }
896             }
897             }
898             else { # Definitely no namespace name, since there's no colon
899 0         0 $category = "Category:$category";
900             }
901 14 50       47 warn "Category to fetch is [[$category]]" if $self->{debug} > 1;
902              
903 14         67 my $hash = {
904             action => 'query',
905             list => 'categorymembers',
906             cmtitle => $category,
907             cmlimit => 'max',
908             };
909 14 50       42 $options->{max} = 1 unless defined($options->{max});
910 14 100       43 delete($options->{max}) if $options->{max} == 0;
911              
912 14         73 my $res = $self->{api}->list($hash, $options);
913 14 100       3757890 return RET_TRUE if not ref $res; # Not a hashref when using callback
914 13 50       38 return $self->_handle_api_error() unless $res;
915              
916 13         52 return map { $_->{title} } @$res;
  2131         2387  
917             }
918              
919              
920             { # Instead of using the state pragma, use a bare block
921             my %data;
922              
923             sub get_all_pages_in_category {
924 14     14 1 3785 my $self = shift;
925 14         22 my $base_category = shift;
926 14         17 my $options = shift;
927 14 100       51 $options->{max} = 0 unless defined($options->{max});
928              
929 14         50 my @first = $self->get_pages_in_category($base_category, $options);
930 14 100       375 %data = () unless $_[0]; # This is a special flag for internal use.
931             # It marks a call to this method as being
932             # internal. Since %data is a fake state variable,
933             # it needs to be cleared for every *external*
934             # call, but not cleared when the call is recursive.
935              
936 14         69 my $ns_data = $self->_get_ns_data();
937 14         40 my $cat_ns_name = $ns_data->{+NS_CATEGORY};
938              
939 14         37 foreach my $page (@first) {
940 2132 100       3086 if ($page =~ m/^$cat_ns_name:/) {
941 18 100       51 if (!exists($data{$page})) {
942 11         30 $data{$page} = '';
943 11         74 my @pages = $self->get_all_pages_in_category($page, $options, 1);
944 11         236 foreach (@pages) {
945 8095         6806 $data{$_} = '';
946             }
947             }
948             else {
949 7         16 $data{$page} = '';
950             }
951             }
952             else {
953 2114         2893 $data{$page} = '';
954             }
955             }
956 14         2365 return keys %data;
957             }
958             } # This ends the bare block around get_all_pages_in_category()
959              
960              
961             sub get_all_categories {
962 2     2 1 1048 my $self = shift;
963 2         3 my $options = shift;
964              
965 2         7 my $query = {
966             action => 'query',
967             list => 'allcategories',
968             };
969              
970 2 100 66     12 if ( defined $options && $options->{'max'} == '0' ) {
971 1         2 $query->{'aclimit'} = 'max';
972             }
973              
974 2         9 my $res = $self->{api}->api($query);
975 2 50       640141 return $self->_handle_api_error() unless $res;
976              
977 2         3 return map { $_->{'*'} } @{ $res->{'query'}->{'allcategories'} };
  510         485  
  2         9  
978             }
979              
980              
981             sub linksearch {
982 2     2 1 1382 my $self = shift;
983 2         3 my $link = shift;
984 2         2 my $ns = shift;
985 2         2 my $prot = shift;
986 2         2 my $options = shift;
987              
988 2 50       6 $ns = join('|', @$ns) if (ref $ns eq 'ARRAY');
989              
990 2         9 my $hash = {
991             action => 'query',
992             list => 'exturlusage',
993             euprop => 'url|title',
994             euquery => $link,
995             eulimit => 'max',
996             };
997 2 50       4 $hash->{eunamespace} = $ns if defined $ns;
998 2 50       4 $hash->{euprotocol} = $prot if $prot;
999 2 50       5 $options->{max} = 1 unless $options->{max};
1000              
1001 2         7 my $res = $self->{api}->list($hash, $options);
1002 2 50       629358 return $self->_handle_api_error() unless $res;
1003 2 100       11 return RET_TRUE if not ref $res; # When using a callback hook, this won't be a reference
1004              
1005 1         2 return map {{
1006             url => $_->{url},
1007             title => $_->{title},
1008 37         59 }} @$res;
1009              
1010             }
1011              
1012              
1013             sub purge_page {
1014 0     0 1 0 my $self = shift;
1015 0         0 my $page = shift;
1016              
1017 0         0 my $hash;
1018 0 0       0 if (ref $page eq 'ARRAY') { # If it is an array reference...
1019 0         0 $hash = {
1020             action => 'purge',
1021             titles => join('|', @$page), # dereference it and purge all those titles
1022             };
1023             }
1024             else { # Just one page
1025 0         0 $hash = {
1026             action => 'purge',
1027             titles => $page,
1028             };
1029             }
1030              
1031 0         0 my $res = $self->{api}->api($hash);
1032 0 0       0 return $self->_handle_api_error() unless $res;
1033 0         0 my $success = 0;
1034 0         0 foreach my $hashref (@{ $res->{purge} }) {
  0         0  
1035 0 0       0 $success++ if exists $hashref->{purged};
1036             }
1037 0         0 return $success;
1038             }
1039              
1040              
1041             sub get_namespace_names {
1042 5     5 1 576 my $self = shift;
1043             my $res = $self->{api}->api({
1044 5         65 action => 'query',
1045             meta => 'siteinfo',
1046             siprop => 'namespaces',
1047             });
1048 5 50       1600982 return $self->_handle_api_error() unless $res;
1049 211         324 return map { $_ => $res->{query}->{namespaces}->{$_}->{'*'} }
1050 5         13 keys %{ $res->{query}->{namespaces} };
  5         45  
1051             }
1052              
1053              
1054             sub image_usage {
1055 3     3 1 4866 my $self = shift;
1056 3         6 my $image = shift;
1057 3         5 my $ns = shift;
1058 3         4 my $filter = shift;
1059 3         6 my $options = shift;
1060              
1061 3 50       16 if ($image !~ m/^File:|Image:/) {
1062 0         0 warnings::warnif('deprecated', q{Please include the canonical File: }
1063             . q{namespace in the image name. If you don't, MediaWiki::Bot might }
1064             . q{incur a network round-trip to get the localized namespace name});
1065 0         0 my $ns_data = $self->_get_ns_data();
1066 0         0 my $file_ns_name = $ns_data->{+NS_FILE};
1067 0 0       0 if ($image !~ m/^\Q$file_ns_name\E:/) {
1068 0         0 $image = "$file_ns_name:$image";
1069             }
1070             }
1071              
1072 3 100       16 $options->{max} = 1 unless defined($options->{max});
1073 3 50       29 delete($options->{max}) if $options->{max} == 0;
1074              
1075 3 50       12 $ns = join('|', @$ns) if (ref $ns eq 'ARRAY');
1076              
1077 3         16 my $hash = {
1078             action => 'query',
1079             list => 'imageusage',
1080             iutitle => $image,
1081             iulimit => 'max',
1082             };
1083 3 50       9 $hash->{iunamespace} = $ns if defined $ns;
1084 3 100 66     20 if (defined($filter) and $filter =~ m/(all|redirects|nonredirects)/) {
1085 1         4 $hash->{'iufilterredir'} = $1;
1086             }
1087 3         16 my $res = $self->{api}->list($hash, $options);
1088 3 50       1196201 return $self->_handle_api_error() unless $res;
1089 3 100       17 return RET_TRUE if not ref $res; # When using a callback hook, this won't be a reference
1090              
1091 2         6 return map { $_->{title} } @$res;
  1000         986  
1092             }
1093              
1094              
1095             sub global_image_usage {
1096 3     3 1 5658 my $self = shift;
1097 3         4 my $image = shift;
1098 3         4 my $limit = shift;
1099 3         3 my $filterlocal = shift;
1100 3 100       9 $limit = defined $limit ? $limit : 500;
1101              
1102 3 100       16 if ($image !~ m/^File:|Image:/) {
1103 1         5 my $ns_data = $self->_get_ns_data();
1104 1         3 my $image_ns_name = $ns_data->{+NS_FILE};
1105 1 50       30 if ($image !~ m/^\Q$image_ns_name\E:/) {
1106 1         5 $image = "$image_ns_name:$image";
1107             }
1108             }
1109              
1110 3         5 my @data;
1111             my $cont;
1112 3 50       17 while ($limit ? scalar @data < $limit : 1) {
1113 3         16 my $hash = {
1114             action => 'query',
1115             prop => 'globalusage',
1116             titles => $image,
1117             # gufilterlocal => $filterlocal,
1118             gulimit => 'max',
1119             };
1120 3 100       8 $hash->{gufilterlocal} = $filterlocal if $filterlocal;
1121 3 50       9 $hash->{gucontinue} = $cont if $cont;
1122              
1123 3         15 my $res = $self->{api}->api($hash);
1124 3 50       955810 return $self->_handle_api_error() unless $res;
1125              
1126 3         11 $cont = $res->{'query-continue'}->{globalusage}->{gucontinue};
1127 3 50 33     11 warn "gucontinue: $cont\n" if $cont and $self->{debug} > 1;
1128 3         5 my $page_id = (keys %{ $res->{query}->{pages} })[0];
  3         13  
1129 3         9 my $results = $res->{query}->{pages}->{$page_id}->{globalusage};
1130 3         62 push @data, @$results;
1131 3 50       38 last unless $cont;
1132             }
1133              
1134 3 100       162 return @data > $limit
1135             ? @data[0 .. $limit-1]
1136             : @data;
1137             }
1138              
1139              
1140             sub links_to_image {
1141 1     1 1 106 warnings::warnif('deprecated', 'links_to_image is an alias of image_usage; '
1142             . 'please use the new name');
1143 1         727 my $self = shift;
1144 1         4 return $self->image_usage($_[0]);
1145             }
1146              
1147              
1148             sub is_blocked {
1149 4     4 1 1293 my $self = shift;
1150 4         6 my $user = shift;
1151              
1152             # http://en.wikipedia.org/w/api.php?action=query&meta=blocks&bkusers=$user&bklimit=1&bkprop=id
1153 4         19 my $hash = {
1154             action => 'query',
1155             list => 'blocks',
1156             bkusers => $user,
1157             bklimit => 1,
1158             bkprop => 'id',
1159             };
1160 4         18 my $res = $self->{api}->api($hash);
1161 4 50       1040431 return $self->_handle_api_error() unless $res;
1162              
1163 4         8 my $number = scalar @{ $res->{query}->{blocks} }; # The number of blocks returned
  4         8  
1164 4 100       17 if ($number == 1) {
    50          
1165 2         21 return RET_TRUE;
1166             }
1167             elsif ($number == 0) {
1168 2         21 return RET_FALSE;
1169             }
1170             else {
1171 0         0 confess "This query should return at most one result, but the API returned more than that.";
1172             }
1173             }
1174              
1175              
1176             sub test_blocked { # For backwards-compatibility
1177 2     2 1 149 warnings::warnif('deprecated', 'test_blocked is an alias of is_blocked; '
1178             . 'please use the new name. This alias might be removed in a future release');
1179 2         1373 return (is_blocked(@_));
1180             }
1181              
1182              
1183             sub test_image_exists {
1184 7     7 1 4390 my $self = shift;
1185 7         12 my $image = shift;
1186              
1187 7         9 my $multi;
1188 7 100       26 if (ref $image eq 'ARRAY') {
1189 1         2 $multi = $image; # so we know to return a hash/scalar & keep track of order
1190 1         5 $image = join('|', @$image);
1191             }
1192              
1193             my $res = $self->{api}->api({
1194 7         66 action => 'query',
1195             titles => $image,
1196             iilimit => 1,
1197             prop => 'imageinfo'
1198             });
1199 7 50       1819252 return $self->_handle_api_error() unless $res;
1200              
1201 7         17 my @sorted_ids;
1202 7 100       21 if ($multi) {
1203 1         2 my %mapped;
1204             $mapped{ $res->{query}->{pages}->{$_}->{title} } = $_
1205 1         2 for (keys %{ $res->{query}->{pages} });
  1         9  
1206 1         2 foreach my $file ( @$multi ) {
1207 3         7 unshift @sorted_ids, $mapped{$file};
1208             }
1209             }
1210             else {
1211 6         10 push @sorted_ids, keys %{ $res->{query}->{pages} };
  6         27  
1212             }
1213 7         11 my @return;
1214 7         18 foreach my $id (@sorted_ids) {
1215 9 100       44 if ($res->{query}->{pages}->{$id}->{imagerepository} eq 'shared') {
    100          
    50          
    50          
1216 3 100       11 if ($multi) {
1217 1         1 unshift @return, FILE_SHARED;
1218             }
1219             else {
1220 2         29 return FILE_SHARED;
1221             }
1222             }
1223             elsif (exists($res->{query}->{pages}->{$id}->{missing})) {
1224 3 100       7 if ($multi) {
1225 1         3 unshift @return, FILE_NONEXISTENT;
1226             }
1227             else {
1228 2         17 return FILE_NONEXISTENT;
1229             }
1230             }
1231             elsif ($res->{query}->{pages}->{$id}->{imagerepository} eq '') {
1232 0 0       0 if ($multi) {
1233 0         0 unshift @return, FILE_PAGE_TEXT_ONLY;
1234             }
1235             else {
1236 0         0 return FILE_PAGE_TEXT_ONLY;
1237             }
1238             }
1239             elsif ($res->{query}->{pages}->{$id}->{imagerepository} eq 'local') {
1240 3 100       8 if ($multi) {
1241 1         3 unshift @return, FILE_LOCAL;
1242             }
1243             else {
1244 2         18 return FILE_LOCAL;
1245             }
1246             }
1247             }
1248              
1249 1         8 return \@return;
1250             }
1251              
1252              
1253             sub get_pages_in_namespace {
1254 4     4 1 2229 my $self = shift;
1255 4         4 my $namespace = shift;
1256 4   100     14 my $limit = shift || 'max';
1257 4         4 my $options = shift;
1258              
1259 4         15 my $hash = {
1260             action => 'query',
1261             list => 'allpages',
1262             apnamespace => $namespace,
1263             aplimit => $limit,
1264             };
1265 4 100       16 $options->{max} = 1 unless defined $options->{max};
1266 4 100 66     21 delete $options->{max} if exists $options->{max} and $options->{max} == 0;
1267              
1268 4         17 my $res = $self->{api}->list($hash, $options);
1269 4 100       3787822 return $self->_handle_api_error() unless $res;
1270 3 50       10 return RET_TRUE if not ref $res; # Not a ref when using callback
1271 3         23 return map { $_->{title} } @$res;
  6179         7816  
1272             }
1273              
1274              
1275             sub count_contributions {
1276 2     2 1 7 my $self = shift;
1277 2         3 my $username = shift;
1278 2         4 $username =~ s/User://i; # Strip namespace
1279              
1280             my $res = $self->{api}->list({
1281 2         15 action => 'query',
1282             list => 'users',
1283             ususers => $username,
1284             usprop => 'editcount'
1285             },
1286             { max => 1 });
1287 2 50       627491 return $self->_handle_api_error() unless $res;
1288 2         3 return ${$res}[0]->{editcount};
  2         13  
1289             }
1290              
1291              
1292             sub timed_count_contributions {
1293 0     0 1 0 my $self = shift;
1294 0         0 my $username = shift;
1295 0         0 my $days = shift;
1296 0         0 $username =~ s/User://i; # Strip namespace
1297              
1298             my $res = $self->{api}->api({
1299 0         0 action => 'userdailycontribs',
1300             user => $username,
1301             daysago => $days,
1302             },
1303             { max => 1 });
1304 0 0       0 return $self->_handle_api_error() unless $res;
1305 0         0 return ($res->{userdailycontribs}->{timeFrameEdits}, $res->{userdailycontribs}->{totalEdits});
1306             }
1307              
1308              
1309             sub last_active {
1310 2     2 1 660 my $self = shift;
1311 2         4 my $username = shift;
1312 2 100       9 $username = "User:$username" unless $username =~ /User:/i;
1313             my $res = $self->{api}->list({
1314 2         16 action => 'query',
1315             list => 'usercontribs',
1316             ucuser => $username,
1317             uclimit => 1
1318             },
1319             { max => 1 });
1320 2 50       614166 return $self->_handle_api_error() unless $res;
1321 2         4 return ${$res}[0]->{timestamp};
  2         13  
1322             }
1323              
1324              
1325             sub recent_edit_to_page {
1326 1     1 1 758 my $self = shift;
1327 1         3 my $page = shift;
1328             my $res = $self->{api}->api({
1329 1         10 action => 'query',
1330             prop => 'revisions',
1331             titles => $page,
1332             rvlimit => 1
1333             },
1334             { max => 1 });
1335 1 50       217921 return $self->_handle_api_error() unless $res;
1336 1         2 my $data = ( %{ $res->{query}->{pages} } )[1];
  1         3  
1337             return ($data->{revisions}[0]->{timestamp},
1338 1         8 $data->{revisions}[0]->{user});
1339             }
1340              
1341              
1342             sub get_users {
1343 1     1 1 29 my $self = shift;
1344 1         2 my $pagename = shift;
1345 1   50     5 my $limit = shift || 'max';
1346 1         3 my $rvstartid = shift;
1347 1         1 my $direction = shift;
1348              
1349 1 50       5 if ($limit > 50) {
1350 0         0 $self->{errstr} = "Error requesting history for $pagename: Limit may not be set to values above 50";
1351 0         0 carp $self->{errstr};
1352 0         0 return;
1353             }
1354 1         8 my $hash = {
1355             action => 'query',
1356             prop => 'revisions',
1357             titles => $pagename,
1358             rvprop => 'ids|timestamp|user|comment',
1359             rvlimit => $limit,
1360             };
1361 1 50       5 $hash->{rvstartid} = $rvstartid if ($rvstartid);
1362 1 50       4 $hash->{rvdir} = $direction if ($direction);
1363              
1364 1         8 my $res = $self->{api}->api($hash);
1365 1 50       228331 return $self->_handle_api_error() unless $res;
1366              
1367 1         1 my ($id) = keys %{ $res->{query}->{pages} };
  1         5  
1368 1         2 return map { $_->{user} } @{$res->{query}->{pages}->{$id}->{revisions}};
  5         21  
  1         3  
1369             }
1370              
1371              
1372             sub was_blocked {
1373 4     4 1 1382 my $self = shift;
1374 4         6 my $user = shift;
1375 4         10 $user =~ s/User://i; # Strip User: prefix, if present
1376              
1377             # http://en.wikipedia.org/w/api.php?action=query&list=logevents&letype=block&letitle=User:127.0.0.1&lelimit=1&leprop=ids
1378 4         28 my $hash = {
1379             action => 'query',
1380             list => 'logevents',
1381             letype => 'block',
1382             letitle => "User:$user", # Ensure the User: prefix is there!
1383             lelimit => 1,
1384             leprop => 'ids',
1385             };
1386              
1387 4         17 my $res = $self->{api}->api($hash);
1388 4 50       1009886 return $self->_handle_api_error() unless $res;
1389              
1390 4         6 my $number = scalar @{ $res->{query}->{logevents} }; # The number of blocks returned
  4         10  
1391 4 100       15 if ($number == 1) {
    50          
1392 2         42 return RET_TRUE;
1393             }
1394             elsif ($number == 0) {
1395 2         25 return RET_FALSE;
1396             }
1397             else {
1398 0         0 confess "This query should return at most one result, but the API returned more than that.";
1399             }
1400             }
1401              
1402              
1403             sub test_block_hist { # Backwards compatibility
1404 2     2 1 149 warnings::warnif('deprecated', 'test_block_hist is an alias of was_blocked; '
1405             . 'please use the new method name. This alias might be removed in a future release');
1406 2         1472 return (was_blocked(@_));
1407             }
1408              
1409              
1410             sub expandtemplates {
1411 2     2 1 11 my $self = shift;
1412 2         4 my $page = shift;
1413 2         2 my $text = shift;
1414              
1415 2 100       6 unless ($text) {
1416 1 50       3 croak q{You must provide a page title} unless $page;
1417 1         4 $text = $self->get_text($page);
1418             }
1419              
1420 2 100       14 my $hash = {
1421             action => 'expandtemplates',
1422             prop => 'wikitext',
1423             ( $page ? (title => $page) : ()),
1424             text => $text,
1425             };
1426 2         10 my $res = $self->{api}->api($hash);
1427 2 50       887490 return $self->_handle_api_error() unless $res;
1428              
1429             return exists $res->{expandtemplates}->{'*'}
1430             ? $res->{expandtemplates}->{'*'}
1431 2 50       45 : $res->{expandtemplates}->{wikitext};
1432             }
1433              
1434              
1435             sub get_allusers {
1436 2     2 1 667 my $self = shift;
1437 2   50     6 my $limit = shift || 'max';
1438 2         2 my $group = shift;
1439 2         3 my $opts = shift;
1440              
1441 2         8 my $hash = {
1442             action => 'query',
1443             list => 'allusers',
1444             aulimit => $limit,
1445             };
1446 2 100       6 $hash->{augroup} = $group if defined $group;
1447 2 50       7 $opts->{max} = 1 unless exists $opts->{max};
1448 2 50 33     11 delete $opts->{max} if exists $opts->{max} and $opts->{max} == 0;
1449 2         8 my $res = $self->{api}->list($hash, $opts);
1450 2 50       618228 return $self->_handle_api_error() unless $res;
1451 2 50       7 return RET_TRUE if not ref $res; # Not a ref when using callback
1452              
1453 2         4 return map { $_->{name} } @$res;
  20         34  
1454             }
1455              
1456              
1457             sub db_to_domain {
1458 1     1 1 9 my $self = shift;
1459 1         1 my $wiki = shift;
1460              
1461 1 50       2 if (!$self->{sitematrix}) {
1462 1         3 $self->_get_sitematrix();
1463             }
1464              
1465 1 50       12 if (ref $wiki eq 'ARRAY') {
1466 1         3 my @return;
1467 1         6 foreach my $w (@$wiki) {
1468 6         16 $wiki =~ s/_p$//; # Strip off a _p suffix, if present
1469 6   100     28 my $domain = $self->{sitematrix}->{$w} || undef;
1470 6 100       27 $domain =~ s/^https\:\/\/// if (defined $domain); # Strip off a https:// prefix, if present
1471 6         15 push(@return, $domain);
1472             }
1473 1         8 return \@return;
1474             }
1475             else {
1476 0         0 $wiki =~ s/_p$//; # Strip off a _p suffix, if present
1477 0   0     0 my $domain = $self->{sitematrix}->{$wiki} || undef;
1478 0 0       0 $domain =~ s/^https\:\/\/// if (defined $domain); # Strip off a https:// prefix, if present
1479 0         0 return $domain;
1480             }
1481             }
1482              
1483              
1484             sub domain_to_db {
1485 1     1 1 1679 my $self = shift;
1486 1         2 my $wiki = shift;
1487              
1488 1 50       6 if (!$self->{sitematrix}) {
1489 0         0 $self->_get_sitematrix();
1490             }
1491              
1492 1 50       8 if (ref $wiki eq 'ARRAY') {
1493 1         3 my @return;
1494 1         4 foreach my $w (@$wiki) {
1495 6 50       23 $w = "https://".$w if ($w !~ /^https\:\//); # Prepend a https:// prefix, if not present
1496 6   100     35 my $db = $self->{sitematrix}->{$w} || undef;
1497 6         14 push(@return, $db);
1498             }
1499 1         6 return \@return;
1500             }
1501             else {
1502 0 0       0 $wiki = "https://".$wiki if ($wiki !~ /^https\:\//); # Prepend a https:// prefix, if not present
1503 0   0     0 my $db = $self->{sitematrix}->{$wiki} || undef;
1504 0         0 return $db;
1505             }
1506             }
1507              
1508              
1509             sub diff {
1510 1     1 1 10 my $self = shift;
1511 1         2 my $title;
1512             my $revid;
1513 0         0 my $oldid;
1514              
1515 1 50       3 if (ref $_[0] eq 'HASH') {
1516 1         2 $title = $_[0]->{title};
1517 1         2 $revid = $_[0]->{revid};
1518 1         2 $oldid = $_[0]->{oldid};
1519             }
1520             else {
1521 0         0 $title = shift;
1522 0         0 $revid = shift;
1523 0         0 $oldid = shift;
1524             }
1525              
1526 1         4 my $hash = {
1527             action => 'query',
1528             prop => 'revisions',
1529             rvdiffto => $oldid,
1530             };
1531 1 50       5 if ($title) {
    50          
1532 0         0 $hash->{titles} = $title;
1533 0         0 $hash->{rvlimit} = 1;
1534             }
1535             elsif ($revid) {
1536 1         2 $hash->{'revids'} = $revid;
1537             }
1538              
1539 1         5 my $res = $self->{api}->api($hash);
1540 1 50       461793 return $self->_handle_api_error() unless $res;
1541 1         2 my @revids = keys %{ $res->{query}->{pages} };
  1         6  
1542 1         4 my $diff = $res->{query}->{pages}->{ $revids[0] }->{revisions}->[0]->{diff}->{'*'};
1543              
1544 1         10 return $diff;
1545             }
1546              
1547              
1548             sub prefixindex {
1549 1     1 1 6 my $self = shift;
1550 1         2 my $prefix = shift;
1551 1         2 my $ns = shift;
1552 1         1 my $filter = shift;
1553 1         2 my $options = shift;
1554              
1555 1 50 33     3 if (defined($filter) and $filter =~ m/(all|redirects|nonredirects)/) { # Verify
1556 0         0 $filter = $1;
1557             }
1558              
1559 1 50 33     6 if (!defined $ns && $prefix =~ m/:/) {
1560 1 50       3 print STDERR "Converted '$prefix' to..." if $self->{debug} > 1;
1561 1         3 my ($name) = split(/:/, $prefix, 2);
1562 1         4 my $ns_data = $self->_get_ns_data();
1563 1         2 $ns = $ns_data->{$name};
1564 1         21 $prefix =~ s/^$name://;
1565 1 50       5 warn "'$prefix' with a namespace filter $ns" if $self->{debug} > 1;
1566             }
1567              
1568 1         6 my $hash = {
1569             action => 'query',
1570             list => 'allpages',
1571             apprefix => $prefix,
1572             aplimit => 'max',
1573             };
1574 1 50       3 $hash->{apnamespace} = $ns if defined $ns;
1575 1 50       3 $hash->{apfilterredir} = $filter if $filter;
1576 1 50       4 $options->{max} = 1 unless $options->{max};
1577              
1578 1         6 my $res = $self->{api}->list($hash, $options);
1579              
1580 1 50       209746 return $self->_handle_api_error() unless $res;
1581 1 50       3 return RET_TRUE if not ref $res; # Not a ref when using callback hook
1582              
1583             return map {
1584 1         3 { title => $_->{title}, redirect => defined $_->{redirect} }
1585 3         14 } @$res;
1586             }
1587              
1588              
1589             sub search {
1590 2     2 1 653 my $self = shift;
1591 2         2 my $term = shift;
1592 2   50     9 my $ns = shift || 0;
1593 2         2 my $options = shift;
1594              
1595 2 50       8 if (ref $ns eq 'ARRAY') { # Accept a hashref
1596 0         0 $ns = join('|', @$ns);
1597             }
1598              
1599 2         9 my $hash = {
1600             action => 'query',
1601             list => 'search',
1602             srsearch => $term,
1603             srwhat => 'text',
1604             srlimit => 'max',
1605              
1606             #srinfo => 'totalhits',
1607             srprop => 'size',
1608             srredirects => 0,
1609             };
1610 2 50       7 $options->{max} = 1 unless $options->{max};
1611              
1612 2         7 my $res = $self->{api}->list($hash, $options);
1613 2 50       788194 return $self->_handle_api_error() unless $res;
1614 2 50       7 return RET_TRUE if not ref $res; # Not a ref when used with callback
1615              
1616 2         26 return map { $_->{title} } @$res;
  141         143  
1617             }
1618              
1619              
1620             sub get_log {
1621 1     1 1 6 my $self = shift;
1622 1         1 my $data = shift;
1623 1         1 my $options = shift;
1624              
1625 1         2 my $log_type = $data->{type};
1626 1         1 my $user = $data->{user};
1627 1         1 my $target = $data->{target};
1628              
1629 1 50       3 if ($user) {
1630 1         2 my $ns_data = $self->_get_ns_data();
1631 1         2 my $user_ns_name = $ns_data->{+NS_USER};
1632 1         12 $user =~ s/^$user_ns_name://;
1633             }
1634              
1635 1         3 my $hash = {
1636             action => 'query',
1637             list => 'logevents',
1638             lelimit => 'max',
1639             };
1640 1 50       4 $hash->{letype} = $log_type if $log_type;
1641 1 50       4 $hash->{leuser} = $user if $user;
1642 1 50       3 $hash->{letitle} = $target if $target;
1643 1 50       5 $options->{max} = 1 unless $options->{max};
1644              
1645 1         5 my $res = $self->{api}->list($hash, $options);
1646 1 50       217773 return $self->_handle_api_error() unless $res;
1647 1 50       4 return RET_TRUE if not ref $res; # Not a ref when using callback
1648              
1649 1         10 return $res;
1650             }
1651              
1652              
1653             sub is_g_blocked {
1654 1     1 1 6 my $self = shift;
1655 1         2 my $ip = shift;
1656              
1657             # http://en.wikipedia.org/w/api.php?action=query&list=globalblocks&bglimit=1&bgprop=address&bgip=127.0.0.1
1658             my $res = $self->{api}->api({
1659 1         9 action => 'query',
1660             list => 'globalblocks',
1661             bglimit => 1,
1662             bgprop => 'address',
1663             # So handy! It searches for blocks affecting this IP or IP range,
1664             # including rangeblocks! Can't get that from UI.
1665             bgip => $ip,
1666             });
1667 1 50       412613 return $self->_handle_api_error() unless $res;
1668 1 50       8 return RET_FALSE unless ($res->{query}->{globalblocks}->[0]);
1669              
1670 0         0 return $res->{query}->{globalblocks}->[0]->{address};
1671             }
1672              
1673              
1674             sub was_g_blocked {
1675 2     2 1 7 my $self = shift;
1676 2         3 my $ip = shift;
1677 2         4 $ip =~ s/User://i; # Strip User: prefix, if present
1678              
1679             # This query should always go to Meta
1680 2 50       7 unless ( $self->{host} eq 'meta.wikimedia.org' ) {
1681 0 0       0 carp "GlobalBlocking queries should probably be sent to Meta; it doesn't look like you're doing so" if $self->{debug};
1682             }
1683              
1684             # http://meta.wikimedia.org/w/api.php?action=query&list=logevents&letype=gblblock&letitle=User:127.0.0.1&lelimit=1&leprop=ids
1685             my $res = $self->{api}->api({
1686 2         16 action => 'query',
1687             list => 'logevents',
1688             letype => 'gblblock',
1689             letitle => "User:$ip", # Ensure the User: prefix is there!
1690             lelimit => 1,
1691             leprop => 'ids',
1692             });
1693              
1694 2 50       646639 return $self->_handle_api_error() unless $res;
1695 2         3 my $number = scalar @{ $res->{query}->{logevents} }; # The number of blocks returned
  2         6  
1696              
1697 2 100       12 if ($number == 1) {
    50          
1698 1         10 return RET_TRUE;
1699             }
1700             elsif ($number == 0) {
1701 1         14 return RET_FALSE;
1702             }
1703             else {
1704 0         0 confess "This query should return at most one result, but the API gave more than that.";
1705             }
1706             }
1707              
1708              
1709             sub was_locked {
1710 2     2 1 670 my $self = shift;
1711 2         4 my $user = shift;
1712              
1713             # This query should always go to Meta
1714 2 50       17 unless (
1715             $self->{api}->{config}->{api_url} =~ m,
1716             \Qhttp://meta.wikimedia.org/w/api.php\E
1717             |
1718             \Qhttps://secure.wikimedia.org/wikipedia/meta/w/api.php\E
1719             ,x # /x flag is pretty awesome :)
1720             )
1721             {
1722 2 50       8 carp "CentralAuth queries should probably be sent to Meta; it doesn't look like you're doing so" if $self->{debug};
1723             }
1724              
1725 2         5 $user =~ s/^User://i;
1726 2         3 $user =~ s/\@global$//i;
1727             my $res = $self->{api}->api({
1728 2         23 action => 'query',
1729             list => 'logevents',
1730             letype => 'globalauth',
1731             letitle => "User:$user\@global",
1732             lelimit => 1,
1733             leprop => 'ids',
1734             });
1735 2 50       589055 return $self->_handle_api_error() unless $res;
1736 2         5 my $number = scalar @{ $res->{query}->{logevents} };
  2         6  
1737 2 100       12 if ($number == 1) {
    50          
1738 1         10 return RET_TRUE;
1739             }
1740             elsif ($number == 0) {
1741 1         6 return RET_FALSE;
1742             }
1743             else {
1744 0         0 confess "This query should return at most one result, but the API returned more than that.";
1745             }
1746             }
1747              
1748              
1749             sub get_protection {
1750 3     3 1 1283 my $self = shift;
1751 3         6 my $page = shift;
1752 3 100       11 if (ref $page eq 'ARRAY') {
1753 1         4 $page = join('|', @$page);
1754             }
1755              
1756 3         17 my $hash = {
1757             action => 'query',
1758             titles => $page,
1759             prop => 'info',
1760             inprop => 'protection',
1761             };
1762 3         15 my $res = $self->{api}->api($hash);
1763 3 50       832878 return $self->_handle_api_error() unless $res;
1764              
1765 3         10 my $data = $res->{query}->{pages};
1766              
1767 3         6 my $out_data;
1768 3         11 foreach my $item (keys %$data) {
1769 4         10 my $title = $data->{$item}->{title};
1770 4         8 my $protection = $data->{$item}->{protection};
1771 4 100       12 if (@$protection == 0) {
1772 3         5 $protection = undef;
1773             }
1774 4         14 $out_data->{$title} = $protection;
1775             }
1776              
1777 3 100       13 if (scalar keys %$out_data == 1) {
1778 2         26 return $out_data->{$page};
1779             }
1780             else {
1781 1         15 return $out_data;
1782             }
1783             }
1784              
1785              
1786             sub is_protected {
1787 1     1 1 95 warnings::warnif('deprecated', 'is_protected is deprecated, and might be '
1788             . 'removed in a future release; please use get_protection instead');
1789 1         758 my $self = shift;
1790 1         4 return $self->get_protection(@_);
1791             }
1792              
1793              
1794             sub patrol {
1795 0     0 1 0 my $self = shift;
1796 0         0 my $rcid = shift;
1797              
1798 0 0       0 if (ref $rcid eq 'ARRAY') {
1799 0         0 my @return;
1800 0         0 foreach my $id (@$rcid) {
1801 0         0 my $res = $self->patrol($id);
1802 0         0 push(@return, $res);
1803             }
1804 0         0 return @return;
1805             }
1806             else {
1807 0         0 my ($token) = $self->_get_edittoken('patrol');
1808             my $res = $self->{api}->api({
1809 0         0 action => 'patrol',
1810             rcid => $rcid,
1811             token => $token,
1812             });
1813             return $self->_handle_api_error()
1814             if !$res
1815 0 0 0     0 or $self->{error}->{details} && $self->{error}->{details} =~ m/^(?:permissiondenied|badtoken)/;
      0        
1816              
1817 0         0 return $res;
1818             }
1819             }
1820              
1821              
1822             sub email {
1823 0     0 1 0 my $self = shift;
1824 0         0 my $user = shift;
1825 0         0 my $subject = shift;
1826 0         0 my $body = shift;
1827              
1828 0 0       0 if (ref $user eq 'ARRAY') {
1829 0         0 my @return;
1830 0         0 foreach my $target (@$user) {
1831 0         0 my $res = $self->email($target, $subject, $body);
1832 0         0 push(@return, $res);
1833             }
1834 0         0 return @return;
1835             }
1836              
1837 0         0 $user =~ s/^User://;
1838 0 0       0 if ($user =~ m/:/) {
1839 0         0 my $user_ns_name = $self->_get_ns_data()->{+NS_USER};
1840 0         0 $user =~ s/^$user_ns_name://;
1841             }
1842              
1843 0         0 my ($token) = $self->_get_edittoken;
1844             my $res = $self->{api}->api({
1845 0         0 action => 'emailuser',
1846             target => $user,
1847             subject => $subject,
1848             text => $body,
1849             token => $token,
1850             });
1851 0 0       0 return $self->_handle_api_error() unless $res;
1852 0         0 return $res;
1853             }
1854              
1855              
1856             sub top_edits {
1857 0     0 1 0 my $self = shift;
1858 0         0 my $user = shift;
1859 0         0 my $options = shift;
1860              
1861 0         0 $user =~ s/^User://;
1862              
1863 0 0       0 $options->{max} = 1 unless defined($options->{max});
1864 0 0       0 delete($options->{max}) if $options->{max} == 0;
1865              
1866 0         0 my $res = $self->{'api'}->list({
1867             action => 'query',
1868             list => 'usercontribs',
1869             ucuser => $user,
1870             ucprop => 'title|flags',
1871             uclimit => 'max',
1872             }, $options);
1873 0 0       0 return $self->_handle_api_error() unless $res;
1874 0 0       0 return RET_TRUE if not ref $res; # Not a ref when using callback
1875              
1876             return
1877 0         0 map { $_->{title} }
1878 0         0 grep { exists $_->{top} }
  0         0  
1879             @$res;
1880             }
1881              
1882              
1883             sub contributions {
1884 3     3 1 5328 my $self = shift;
1885 3         7 my $user = shift;
1886 3         4 my $ns = shift;
1887 3         3 my $opts = shift;
1888              
1889 3 100       13 if (ref $user eq 'ARRAY') {
1890 1         9 $user = join '|', map { my $u = $_; $u =~ s{^User:}{}; $u } @$user;
  2         2  
  2         7  
  2         4  
1891             }
1892             else {
1893 2         5 $user =~ s{^User:}{};
1894             }
1895 3 50       11 $ns = join '|', @$ns
1896             if ref $ns eq 'ARRAY';
1897              
1898 3 50       15 $opts->{max} = 1 unless defined($opts->{max});
1899 3 50       9 delete($opts->{max}) if $opts->{max} == 0;
1900              
1901 3 100       21 my $query = {
1902             action => 'query',
1903             list => 'usercontribs',
1904             ucuser => $user,
1905             ( defined $ns ? (ucnamespace => $ns) : ()),
1906             ucprop => 'ids|title|timestamp|comment|flags',
1907             uclimit => 'max',
1908             };
1909 3         14 my $res = $self->{api}->list($query, $opts);
1910 3 50       1062183 return $self->_handle_api_error() unless $res->[0];
1911 3 50       12 return RET_TRUE if not ref $res; # Not a ref when using callback
1912              
1913 3         108 return @$res;
1914             }
1915              
1916              
1917             sub upload {
1918 0     0 1 0 my $self = shift;
1919 0         0 my $args = shift;
1920              
1921 0         0 my $data = delete $args->{data};
1922 0 0 0     0 if (!defined $data and defined $args->{file}) {
1923 0 0       0 $data = do { local $/; open my $in, '<:raw', $args->{file} or die $!; <$in> };
  0         0  
  0         0  
  0         0  
1924             }
1925 0 0       0 unless (defined $data) {
1926 0         0 $self->{error}->{code} = ERR_PARAMS;
1927 0         0 $self->{error}->{details} = q{You must provide either file contents or a filename.};
1928 0         0 return undef;
1929             }
1930 0 0 0     0 unless (defined $args->{file} or defined $args->{title}) {
1931 0         0 $self->{error}->{code} = ERR_PARAMS;
1932 0         0 $self->{error}->{details} = q{You must specify a title to upload to.};
1933 0         0 return undef;
1934             }
1935              
1936 0   0     0 my $filename = $args->{title} || do { require File::Basename; File::Basename::basename($args->{file}) };
1937             my $success = $self->{api}->edit({
1938             action => 'upload',
1939             filename => $filename,
1940             comment => $args->{summary},
1941 0   0     0 file => [ undef, $filename, Content => $data ],
1942             }) || return $self->_handle_api_error();
1943 0         0 return $success;
1944             }
1945              
1946              
1947             sub upload_from_url {
1948 0     0 1 0 my $self = shift;
1949 0         0 my $args = shift;
1950              
1951 0         0 my $url = delete $args->{url};
1952 0 0       0 unless (defined $url) {
1953 0         0 $self->{error}->{code} = ERR_PARAMS;
1954 0         0 $self->{error}->{details} = q{You must provide URL of file to upload.};
1955 0         0 return undef;
1956             }
1957              
1958 0   0     0 my $filename = $args->{title} || do {
1959             require File::Basename;
1960             File::Basename::basename($url)
1961             };
1962             my $success = $self->{api}->edit({
1963             action => 'upload',
1964             filename => $filename,
1965             comment => $args->{summary},
1966 0   0     0 url => $url,
1967             ignorewarnings => 1,
1968             }) || return $self->_handle_api_error();
1969 0         0 return $success;
1970             }
1971              
1972              
1973              
1974             sub usergroups {
1975 1     1 1 5 my $self = shift;
1976 1         1 my $user = shift;
1977              
1978 1         2 $user =~ s/^User://;
1979              
1980             my $res = $self->{api}->api({
1981 1         7 action => 'query',
1982             list => 'users',
1983             ususers => $user,
1984             usprop => 'groups',
1985             ustoken => 'userrights',
1986             });
1987 1 50       489838 return $self->_handle_api_error() unless $res;
1988              
1989 1         2 foreach my $res_user (@{ $res->{query}->{users} }) {
  1         3  
1990 1 50       3 next unless $res_user->{name} eq $user;
1991              
1992             # Cache the userrights token on the assumption that we'll use it shortly to change the rights
1993             $self->{userrightscache} = {
1994             user => $user,
1995             token => $res_user->{userrightstoken},
1996             groups => $res_user->{groups},
1997 1         7 };
1998              
1999 1         2 return @{ $res_user->{groups} }; # SUCCESS
  1         7  
2000             }
2001              
2002 0         0 return $self->_handle_api_error({ code => ERR_API, details => qq{Results for $user weren't returned by the API} });
2003             }
2004              
2005              
2006             ################
2007             # Internal use #
2008             ################
2009              
2010             sub _get_edittoken { # Actually returns ($token, $base_timestamp, $start_timestamp)
2011 1     1   1 my $self = shift;
2012 1   50     3 my $page = shift || 'Main Page';
2013 1   50     6 my $type = shift || 'csrf';
2014              
2015             my $res = $self->{api}->api({
2016 1 50       8 action => 'query',
2017             meta => 'siteinfo|tokens',
2018             titles => $page,
2019             prop => 'revisions',
2020             rvprop => 'timestamp',
2021             type => $type,
2022             }) or return $self->_handle_api_error();
2023              
2024 1         413219 my $data = ( %{ $res->{query}->{pages} })[1];
  1         5  
2025 1         3 my $base_timestamp = $data->{revisions}[0]->{timestamp};
2026 1         2 my $start_timestamp = $res->{query}->{general}->{time};
2027 1         3 my $token = $res->{query}->{tokens}->{"${type}token"};
2028              
2029 1         11 return ($token, $base_timestamp, $start_timestamp);
2030             }
2031              
2032             sub _handle_api_error {
2033 3     3   9 my $self = shift;
2034 3         6 my $error = shift;
2035              
2036 3         12 $self->{error} = {};
2037              
2038             carp 'Error code '
2039             . $self->{api}->{error}->{code}
2040             . ': '
2041 3 50       12 . $self->{api}->{error}->{details} if $self->{debug};
2042             $self->{error} =
2043             (defined $error and ref $error eq 'HASH' and exists $error->{code} and exists $error->{details})
2044             ? $error
2045 3 50 0     22 : $self->{api}->{error};
2046              
2047 3         21 return undef;
2048             }
2049              
2050             sub _is_loggedin {
2051 0     0   0 my $self = shift;
2052              
2053 0   0     0 my $is = $self->_whoami() || return $self->_handle_api_error();
2054 0         0 my $ought = $self->{username};
2055 0 0       0 warn "Testing if logged in: we are $is, and we should be $ought" if $self->{debug} > 1;
2056 0         0 return ($is eq $ought);
2057             }
2058              
2059             sub _whoami {
2060 0     0   0 my $self = shift;
2061              
2062             my $res = $self->{api}->api({
2063 0 0       0 action => 'query',
2064             meta => 'userinfo',
2065             }) or return $self->_handle_api_error();
2066              
2067 0         0 return $res->{query}->{userinfo}->{name};
2068             }
2069              
2070             sub _do_autoconfig {
2071 0     0   0 my $self = shift;
2072              
2073             # http://en.wikipedia.org/w/api.php?action=query&meta=userinfo&uiprop=rights|groups
2074 0         0 my $hash = {
2075             action => 'query',
2076             meta => 'userinfo',
2077             uiprop => 'rights|groups',
2078             };
2079 0         0 my $res = $self->{api}->api($hash);
2080 0 0       0 return $self->_handle_api_error() unless $res;
2081 0 0       0 return $self->_handle_api_error() unless $res->{query};
2082 0 0       0 return $self->_handle_api_error() unless $res->{query}->{userinfo};
2083 0 0       0 return $self->_handle_api_error() unless $res->{query}->{userinfo}->{name};
2084              
2085 0         0 my $is = $res->{query}->{userinfo}->{name};
2086 0         0 my $ought = $self->{username};
2087              
2088             # Should we try to recover by logging in again? croak?
2089 0 0       0 carp "We're logged in as $is but we should be logged in as $ought" if ($is ne $ought);
2090              
2091 0 0       0 my @rights = @{ $res->{query}->{userinfo}->{rights} || [] };
  0         0  
2092 0         0 my $has_bot = 0;
2093 0         0 my $default_assert = 'user'; # At a *minimum*, the bot should be logged in.
2094 0         0 foreach my $right (@rights) {
2095 0 0       0 if ($right eq 'bot') {
2096 0         0 $has_bot = 1;
2097 0         0 $default_assert = 'bot';
2098             }
2099             }
2100              
2101 0 0       0 my @groups = @{ $res->{query}->{userinfo}->{groups} || [] }; # there may be no groups
  0         0  
2102 0         0 my $is_sysop = 0;
2103 0         0 foreach my $group (@groups) {
2104 0 0       0 if ($group eq 'sysop') {
2105 0         0 $is_sysop = 1;
2106             }
2107             }
2108              
2109 0 0 0     0 unless ($has_bot && !$is_sysop) {
2110 0 0       0 warn "$is doesn't have a bot flag; edits will be visible in RecentChanges" if $self->{debug} > 1;
2111             }
2112 0 0       0 $self->{assert} = $default_assert unless $self->{assert};
2113              
2114 0         0 return RET_TRUE;
2115             }
2116              
2117             sub _get_sitematrix {
2118 1     1   2 my $self = shift;
2119              
2120 1         4 my $res = $self->{api}->api({ action => 'sitematrix' });
2121 1 50       465011 return $self->_handle_api_error() unless $res;
2122 1         2 my %sitematrix = %{ $res->{sitematrix} };
  1         78  
2123              
2124             # This hash is a monstrosity (see http://sprunge.us/dfBD?pl), and needs
2125             # lots of post-processing to have a sane data structure :\
2126 1         9 my %by_db;
2127 1         19 SECTION: foreach my $hashref (%sitematrix) {
2128 590 100       679 if (ref $hashref ne 'HASH') { # Yes, there are non-hashrefs in here, wtf?!
2129 297 100       311 if ($hashref eq 'specials') {
2130 1         2 SPECIAL: foreach my $special (@{ $sitematrix{specials} }) {
  1         3  
2131             next SPECIAL
2132             if (exists($special->{private})
2133 91 100 66     145 or exists($special->{fishbowl}));
2134              
2135 55         36 my $db = $special->{code};
2136 55         33 my $domain = $special->{url};
2137 55         36 $domain =~ s,^http://,,;
2138              
2139 55         68 $by_db{$db} = $domain;
2140             }
2141             }
2142 297         239 next SECTION;
2143             }
2144              
2145 293         205 my $lang = $hashref->{code};
2146              
2147 293         202 WIKI: foreach my $wiki_ref ($hashref->{site}) {
2148 293         240 WIKI2: foreach my $wiki_ref2 (@$wiki_ref) {
2149 805         563 my $family = $wiki_ref2->{code};
2150 805         498 my $domain = $wiki_ref2->{url};
2151 805         503 $domain =~ s,^http://,,;
2152              
2153 805         530 my $db = $lang . $family; # Is simple concatenation /always/ correct?
2154              
2155 805         1083 $by_db{$db} = $domain;
2156             }
2157             }
2158             }
2159              
2160             # Now filter out closed wikis
2161 1         15 my $response = $self->{api}->{ua}->get('http://noc.wikimedia.org/conf/closed.dblist');
2162 1 50       301390 if ($response->is_success()) {
2163 1         27 my @closed_list = split(/\n/, $response->decoded_content);
2164 1         164 CLOSED: foreach my $closed (@closed_list) {
2165 130         251 delete($by_db{$closed});
2166             }
2167             }
2168              
2169             # Now merge in the reverse, so you can look up by domain as well as db
2170 1         3 my %by_domain;
2171 1         9 while (my ($key, $value) = each %by_db) {
2172 748         4899 $by_domain{$value} = $key;
2173             }
2174 1         1025 %by_db = (%by_db, %by_domain);
2175              
2176             # This could be saved to disk with Storable. Next time you call this
2177             # method, if mtime is less than, say, 14d, you could load it from
2178             # disk instead of over network.
2179 1         192 $self->{sitematrix} = \%by_db;
2180              
2181 1         1342 return $self->{sitematrix};
2182             }
2183              
2184             sub _get_ns_data {
2185 17     17   22 my $self = shift;
2186              
2187             # If we have it already, return the cached data
2188 17 100       70 return $self->{ns_data} if exists $self->{ns_data};
2189              
2190             # If we haven't returned by now, we have to ask the API
2191 4         19 my %ns_data = $self->get_namespace_names();
2192 4         226 my %reverse = reverse %ns_data;
2193 4         177 %ns_data = (%ns_data, %reverse);
2194 4         54 $self->{ns_data} = \%ns_data; # Save for later use
2195              
2196 4         28 return $self->{ns_data};
2197             }
2198              
2199             sub _get_ns_alias_data {
2200 3     3   2657 my $self = shift;
2201              
2202 3 100       12 return $self->{ns_alias_data} if exists $self->{ns_alias_data};
2203              
2204             my $ns_res = $self->{api}->api({
2205 2         17 action => 'query',
2206             meta => 'siteinfo',
2207             siprop => 'namespacealiases|namespaces',
2208             });
2209              
2210             my %ns_alias_data =
2211             map { # Map namespace alias names like "WP" to the canonical namespace name
2212             # from the "namespaces" part of the response
2213             $_->{ns_alias} => $ns_res->{query}->{namespaces}->{ $_->{ns_number} }->{canonical}
2214 8         26 }
2215             map { # Map namespace alias names (from the "namespacealiases" part of the response)
2216             # like "WP" to the namespace number (usd to look up canonical data in the
2217             # "namespaces" part of the response)
2218             { ns_alias => $_->{'*'}, ns_number => $_->{id} }
2219 2         502030 } @{ $ns_res->{query}->{namespacealiases} };
  8         17  
  2         8  
2220              
2221 2         11 $self->{ns_alias_data} = \%ns_alias_data;
2222 2         40 return $self->{ns_alias_data};
2223             }
2224              
2225              
2226             1;
2227              
2228             __END__