File Coverage

blib/lib/MediaWiki/Bot.pm
Criterion Covered Total %
statement 831 1128 73.6
branch 306 604 50.6
condition 56 160 35.0
subroutine 64 76 84.2
pod 58 58 100.0
total 1315 2026 64.9


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