File Coverage

blib/lib/Wiki/Toolkit/Store/Mediawiki.pm
Criterion Covered Total %
statement 33 795 4.1
branch 0 548 0.0
condition 0 90 0.0
subroutine 11 59 18.6
pod 29 29 100.0
total 73 1521 4.8


line stmt bran cond sub pod time code
1             package Wiki::Toolkit::Store::Mediawiki;
2              
3 1     1   27402 use warnings;
  1         2  
  1         29  
4 1     1   5 use strict;
  1         2  
  1         32  
5              
6 1     1   31446 use Wiki::Toolkit::Store::Database;
  1         161673  
  1         41  
7 1     1   14 use base qw(Wiki::Toolkit::Store::Database);
  1         2  
  1         133  
8              
9 1     1   1240 use utf8;
  1         12  
  1         6  
10 1     1   44 use Carp qw/carp croak confess/;
  1         3  
  1         90  
11 1     1   7 use Digest::MD5 qw(md5_hex);
  1         2  
  1         52  
12 1     1   6225 use Data::Dumper;
  1         11764  
  1         137  
13 1     1   1628 use Time::Piece::Adaptive qw(:override);
  1         4924  
  1         5  
14 1     1   227 use Time::Seconds;
  1         2  
  1         113  
15 1     1   7 use Scalar::Util qw(reftype);
  1         2  
  1         18278  
16              
17              
18              
19             =head1 NAME
20              
21             Wiki::Toolkit::Store::Mediawiki - Mediawiki (MySQL) storage backend for
22             Wiki::Toolkit
23              
24             =head1 VERSION
25              
26             Version 0.06
27              
28             =cut
29              
30             our $VERSION = '0.06';
31              
32             =head1 REQUIRES
33              
34             Subclasses Wiki::Toolkit::Store::Database.
35              
36             =head1 SYNOPSIS
37              
38             Implementation of L which reads and writes to a
39             Mediawiki 1.6 database running in MySQL. This is module is intended to be
40             capable of running concurrently with a Mediawiki 1.6 installation without
41             data corruption. That said, use it at your own risk.
42              
43             If you are looking for a general Wiki implementation, you might be better off
44             looking at L. It is simpler, more general, and does not
45             require the database to be initialized by outside software. Currently,
46             initializing the database for this module requires a working (PHP) Mediawiki
47             installation.
48              
49             I initially wrote this module because I was sick of running both PHP and Perl
50             on my web server so that I could have the only wiki I could find with the
51             full featureset I wanted running in parallel with the Perl scripts which
52             generate the rest of my content dynamically. Generating my Perl content was
53             much faster than my Mediawiki installation and I like Perl better, so PHP lost.
54             Converting the old Mediawiki database into a format that a less fully featured
55             wiki could read looked generally unrewarding, so here we are.
56              
57             All date and time values are returned as L objects.
58             This should be transparent for most uses.
59              
60             See L for more on the general API.
61              
62             =cut
63              
64              
65              
66             ###
67             ### Globals
68             ###
69             our $timestamp_fmt = "%Y%m%d%H%M%S";
70              
71              
72              
73             # Internal method to return the data source string required by DBI.
74             sub _dsn {
75 0     0     my ($self, $dbname, $dbhost) = @_;
76 0           my $dsn = "dbi:mysql:$dbname";
77 0 0         $dsn .= ";mysql_enable_utf8=1" if $self->{_charset}=~/^utf-?8$/i;
78 0 0         $dsn .= ";host=$dbhost" if $dbhost;
79 0           return $dsn;
80             }
81              
82              
83              
84             =head1 METHODS
85              
86             =head2 check_and_write_node
87              
88             $store->check_and_write_node (node => $node,
89             checksum => $checksum,
90             %other_args);
91              
92             Locks the node, verifies the checksum, calls
93             C with all supplied arguments, unlocks the
94             node. Returns 1 on successful writing, 0 if checksum doesn't match,
95             croaks on error.
96              
97             Note: Uses MySQL's user level locking, so any locks are released when
98             the database handle disconnects. Doing it like this because I can't seem
99             to get it to work properly with transactions.
100              
101             =cut
102              
103             sub check_and_write_node
104             {
105 0     0 1   my ($self, %args) = @_;
106 0           my ($node, $checksum) = @args{qw(node checksum)};
107 0 0         $self->_lock_node ($node) or croak "Can't lock node";
108 0           my $ok = $self->verify_checksum ($node, $checksum);
109 0 0         unless ($ok)
110             {
111 0 0         $self->_unlock_node ($node) or carp "Can't unlock node";
112 0           return 0;
113             }
114 0           eval {$self->write_node_post_locking (%args)};
  0            
115 0           my $saverr = $@;
116 0 0         $self->_unlock_node ($node) or carp "Can't unlock node";
117 0 0         croak $saverr if $saverr;
118 0           return 1;
119             }
120              
121              
122              
123             =head2 new
124              
125             Like the C function from C, but also requires
126             a `wikiname' argument.
127              
128             =cut
129              
130             sub new {
131 0     0 1   my ($class, %args) = @_;
132 0           my $self = {};
133 0           bless $self, $class;
134              
135             # wikiname is required
136 0 0         croak "missing required `wikiname' argument" unless $args{wikiname};
137 0           $self->{wikiname} = $args{wikiname};
138              
139             # Set defaults for these arguments.
140 0 0         if (exists $args{convert_spaces}) {
141 0           $self->{convert_spaces} = $args{convert_spaces};
142             } else {
143 0           $self->{convert_spaces} = 1;
144             }
145              
146 0 0         $self->{default_date_format} = $args{default_date_format}
147             if $args{default_date_format};
148              
149 0 0         if (exists $args{ignore_case}) {
150 0           $self->{ignore_case} = $args{ignore_case};
151             }
152            
153 0 0         $args{charset} = 'utf-8'
154             unless (exists $args{charset});
155              
156             # Call the parent initializer.
157 0           return $self->_init (%args);
158             }
159              
160             # Returns 1 if we can get a lock, 0 if we can't, croaks on error.
161             sub _lock_node
162             {
163 0     0     my ($self, $node) = @_;
164 0           my $dbh = $self->{_dbh};
165 0           $node = $dbh->quote ($node);
166 0           my $sql = "SELECT GET_LOCK($node, 10)";
167 0           my $sth = $dbh->prepare($sql);
168 0 0         $sth->execute or croak $dbh->errstr;
169 0           my $locked = $sth->fetchrow_array;
170 0           $sth->finish;
171 0           return $locked;
172             }
173              
174             # Returns 1 if we can unlock, 0 if we can't, croaks on error.
175             sub _unlock_node {
176 0     0     my ($self, $node) = @_;
177 0           my $dbh = $self->{_dbh};
178 0           $node = $dbh->quote($node);
179 0           my $sql = "SELECT RELEASE_LOCK($node)";
180 0           my $sth = $dbh->prepare($sql);
181 0 0         $sth->execute or croak $dbh->errstr;
182 0           my $unlocked = $sth->fetchrow_array;
183 0           $sth->finish;
184 0           return $unlocked;
185             }
186              
187              
188              
189             our @namespaces = qw{Talk User User_talk Project Project_talk Image Image_talk
190             MediaWiki MediaWiki_talk Template Template_talk Help
191             Help_talk Category Category_talk};
192            
193             # $store->__namespace_to_num ($node_name);
194             #
195             # Translate a node name containing a `:' into a Mediawiki namespace number.
196             sub __namespace_to_num
197             {
198 0     0     my ($self, $name) = @_;
199 0 0         $name =~ s/ /_/g if $self->{convert_spaces};
200 0 0         return 0, $name unless $name =~ /^(?::+)?([^:]+):+([^:].*)$/;
201 0 0         return -2, $2 if $1 eq 'Media';
202 0 0         return -1, $2 if $1 eq 'Special';
203 0 0         return 4, $2 if $1 eq $self->{wikiname};
204 0           for (0 .. $#namespaces)
205             {
206 0 0         return $_ + 1, $2 if $1 eq $namespaces[$_];
207             }
208 0           return 0, $name;
209             }
210              
211              
212              
213             # $store->__num_to_namespace ($namespace_code, $node_name);
214             #
215             # Translate a Mediawiki namespace number into a node name containing a `:'.
216             sub __num_to_namespace
217             {
218 0     0     my ($self, $num, $name) = @_;
219 0 0         $name =~ s/_/ /g if $self->{convert_spaces};
220 0 0         return $name unless $num;
221 0 0         return "Media:$name" if $num == -2;
222 0 0         return "Special:$name" if $num == -1;
223 0 0         return $self->{wikiname} . ":$name" if $num == 4;
224 0 0 0       die "no such namespace $num"
225             unless $num > 0 && $num <= @namespaces;
226 0           return "$namespaces[$num - 1]:$name";
227             }
228              
229              
230              
231             # turn the Wiki::Toolkit metadata fields of a search into a metadata hash
232             # substructure.
233             my @metadata_fields = qw{comment edit_type patrolled username};
234             sub _make_metadata
235             {
236 0     0     my $data = shift;
237 0           my %metadata;
238 0           @metadata{@metadata_fields} = map { [$_] } @$data{@metadata_fields};
  0            
239 0           $data->{metadata} = \%metadata;
240             }
241              
242              
243              
244             sub _make_date
245             {
246 0     0     my ($self, $date) = @_;
247 0           my $newdate;
248 0 0         my @strptime_args = ($date ? $date : "19700101000000", $timestamp_fmt);
249 0 0         push @strptime_args, stringify => $self->{default_date_format}
250             if $self->{default_date_format};
251 0           eval {
252 0           $newdate = Time::Piece::Adaptive->strptime (@strptime_args);
253             };
254 0 0         croak "bad timestamp (`$date').\n", $@ if $@;
255 0           return $newdate;
256             }
257              
258              
259              
260             # UTF-8 decode the elements of an array, an array of rows, an arrayref, or an
261             # arrayref of rows.
262             # # utf8::decode each (sets utf8 flag when necessary)
263             # # Return the original list
264             sub _utf8_on_array
265             {
266 0     0     foreach (@_) {
267 0 0         if (ref $_){ #called via selectall_arrayref
268 0           _utf8_on_array (@$_);
269             } else { #called via selectrow_arrayref
270 0           utf8::decode $_;
271             }
272             }
273              
274 0 0         return @_ if wantarray;
275 0           return $_[0];
276             } #_utf8_on_array
277              
278              
279              
280             =begin :internal
281              
282             =head2 _retrieve_node_data
283              
284             $store->_retrieve_node_data (name => 'Node Name', nometadata => 1);
285              
286             or
287              
288             $store->_retrieve_node_data (version => 1);
289              
290             One, and only one, of C or C is required. When C is
291             supplied, then the most recent version of the node is returned. With
292             C, data for a specific version of a node is returned.
293              
294             Returns a hash of node data and metadata when called in array context and
295             just the raw content of the node in scalar context.
296              
297             =end :internal
298              
299             =cut
300              
301             sub _retrieve_node_data
302             {
303 0     0     my ($self, %args) = @_;
304 0 0 0       croak "Need name or version to lookup node"
305             unless $args{name} || $args{version};
306 0           my $dbh = $self->dbh;
307 0           my $sql;
308             my %data;
309 0           my @outfields = qw{content last_modified};
310 0           my $infields;
311 0 0         my $ignore_case = defined $args{ignore_case}
312             ? $args{ignore_case} : $self->{ignore_case};
313              
314 0 0         if ($args{version})
315             {
316 0 0         croak "version argument `$args{version}' is not numeric."
317             unless $args{version} =~ /^\d+$/;
318              
319 0           $infields = "old_text";
320 0 0         if (wantarray)
321             {
322 0           push @outfields, qw{ns name};
323 0           $infields .= ", rc_timestamp, rc_namespace, rc_title";
324 0 0         unless ($args{nometadata})
325             {
326 0           push @outfields, qw{edit_type username comment patrolled restrictions};
327 0           $infields .= ", rc_minor, rc_user_text, rc_comment, rc_patrolled, page_restrictions";
328             }
329             }
330 0           $sql = "SELECT $infields "
331             . "FROM text, page,"
332             . "(SELECT rc_this_oldid, rc_user_text, rc_comment, "
333             . "rc_timestamp, rc_minor, rc_namespace, rc_title, "
334             . "rc_new, rc_patrolled "
335             . "FROM ((SELECT rc_this_oldid, rc_user_text, rc_comment, "
336             . "rc_timestamp, rc_minor, rc_namespace, "
337             . "rc_title "
338             . "FROM recentchanges "
339             . "WHERE rc_this_oldid = $args{version}) "
340             . "UNION "
341             . "(SELECT rev_text_id, rev_user_text, rev_comment, "
342             . "rev_timestamp, rev_minor_edit, "
343             . "page_namespace AS rev_namespace, "
344             . "page_title AS rev_title "
345             . "FROM revision JOIN page ON page_id = rev_page "
346             . "WHERE rev_text_id = $args{version})) AS b "
347             . "NATURAL LEFT JOIN "
348             . "(SELECT rc_this_oldid, rc_new, rc_patrolled "
349             . "FROM recentchanges) AS pat) AS extra "
350             . "WHERE rc_this_oldid = old_id AND old_id = $args{version} "
351             . "AND rc_title = page_title AND rc_namespace = page_namespace";
352             }
353             else
354             {
355 0           my ($ns, $name) = $self->__namespace_to_num ($args{name});
356 0           $infields = "old_text";
357 0 0         if (wantarray)
358             {
359 0           push @outfields, qw{version};
360 0           $infields .= ", page_touched, page_latest";
361 0 0         if ($ignore_case)
362             {
363 0           push @outfields, qw{ns name};
364 0           $infields .= ", rc_namespace, rc_title";
365             }
366 0 0         unless ($args{nometadata})
367             {
368 0           push @outfields, qw{edit_type username comment restrictions};
369 0           $infields .= ", rc_minor, rc_user_text, rc_comment, page_restrictions";
370             }
371             }
372 0           $sql = "SELECT $infields"
373             . " FROM text, page,"
374             . " (SELECT * FROM (SELECT rc_this_oldid, rc_user_text, rc_comment,"
375             . " rc_timestamp, rc_minor, rc_namespace, rc_title FROM recentchanges) as revPageRc"
376             . " UNION ALL"
377             . " SELECT * FROM (SELECT rev_text_id, rev_user_text, rev_comment,"
378             . " rev_timestamp, rev_minor_edit, page_namespace as rev_namespace,"
379             . " page_title as rev_title FROM revision"
380             . " JOIN page ON page_id=rev_page) as revPage) as allChanges"
381             . " WHERE page_latest = old_id"
382             . " AND page_namespace = $ns"
383             . " AND "
384             . $self->_get_cmp_sql ("page_title",
385             $name,
386             $args{ignore_case})
387             . " AND page_latest = rc_this_oldid GROUP BY page_touched";
388             }
389 0           my @results = _utf8_on_array $dbh->selectrow_array ($sql);
390              
391             # If the user only wanted the node content, we're done
392 0 0         return @results ? $results[0] : "" unless wantarray;
    0          
393              
394             # If nothing was selected, we're also done.
395 0 0         return () unless @results;
396              
397             # Start by copying the results.
398 0           @data{@outfields} = @results;
399              
400             # Then normalize them.
401 0 0         $data{version} = $args{version} if $args{version};
402 0 0 0       if ($args{version} || $ignore_case)
403             {
404 0           $data{name} = $self->__num_to_namespace ($data{ns}, $data{name});
405             }
406             else
407             {
408 0           $data{name} = $args{name};
409             }
410              
411 0 0         croak "No restrictions found for $data{name}. If the content of this"
412             . " database was created exclusively by MediaWiki, this method needs"
413             . " updating to default to autoconfirm for all action types."
414             unless $data{restrictions};
415              
416             #make restrictions string into a nice hash of things ex: move => sysop
417 0 0         if ($data{restrictions} =~ /.*:.*/) {
418 0           my @options = split /:/, $data{restrictions};
419 0           $data{restrictions} = {};
420 0           foreach my $opt (@options) {
421 0           my ($key, $value) = split /=/, $opt, 2;
422 0           $data{restrictions}{$key} = [split /,/, $value];
423             }
424             } else {
425 0           my $data = $data{restrictions};
426 0           $data{restrictions} = {};
427 0           push @{$data{restrictions}{edit}}, $data;
  0            
428 0           push @{$data{restrictions}{move}}, $data;
  0            
429             }
430              
431 0 0         $data{edit_type} = $data{edit_type} ? "Minor tidying" : "Normal edit"
    0          
432             if defined $data{edit_type};
433 0           $data{last_modified} = $self->_make_date ($data{last_modified});
434 0 0         _make_metadata \%data unless $args{nometadata};
435              
436 0           return %data;
437             }
438              
439              
440              
441             # $store->_retrieve_node_content (name => $node_name,
442             # version => $node_version);
443             # Params: 'name' is compulsory, 'version' is optional and defaults to latest.
444             # Returns a hash of data for C - content, version, last modified,
445             # or scalar, depending on context.
446             sub _retrieve_node_content
447             {
448 0     0     return _retrieve_node_data @_, nometadata => 1;
449             }
450              
451              
452              
453             =head2 list_all_nodes
454              
455             Like the parent function, but accepts metadata_is, metadata_isnt, limit,
456             & offset arguments.
457              
458             =cut
459              
460             sub list_all_nodes
461             {
462 0     0 1   my ($self, %args) = @_;
463 0           my $dbh = $self->dbh;
464              
465 0 0 0       my $where .= " WHERE 1 = 1"
466             if $args{metadata_is} || $args{metadata_isnt};
467 0 0         $where .= $self->_get_metadata_sql (1, "page_", $args{metadata_is}, %args)
468             if $args{metadata_is};
469 0 0         $where .= $self->_get_metadata_sql (0, "page_", $args{metadata_isnt}, %args)
470             if $args{metadata_isnt};
471 0           my $limoffsql = _get_lim_off_sql (%args);
472 0 0         $where .= " " . $limoffsql if $limoffsql;
473              
474 0 0         if($args{with_details}) {
475 0           my $sql = "SELECT rc_namespace, rc_title, rc_patrolled FROM"
476             . " (SELECT * FROM recentchanges WHERE rc_namespace >= 0"
477             . " ORDER BY rc_timestamp DESC) as latest"
478             . " GROUP BY rc_title, rc_namespace";
479 0           my $patrolled = $dbh->selectall_arrayref ($sql);
480 0           my %rc;
481 0           foreach my $rc_page (@{$patrolled}) {
  0            
482 0           $rc{$rc_page->[0].$rc_page->[1]} = $rc_page->[2];
483             }
484              
485 0           $sql = "SELECT page_id, page_namespace, page_title ,page_latest FROM page";
486 0           my @nodes;
487              
488 0           my $results = $dbh->selectall_arrayref ($sql.$where);
489 0           foreach my $page (@{$results}){
  0            
490 0 0         my %data = (node_id => $page->[0],
491             name => $self->__num_to_namespace ($page->[1], $page->[2]),
492             version => $page->[3],
493             moderate => $rc{$page->[1].$page->[2]} ?
494             $rc{$page->[1].$page->[2]} : '1');
495 0           push @nodes, \%data;
496             }
497 0           return @nodes;
498             } else {#just names
499 0           my $fields;
500              
501 0 0         if (wantarray)
502             {
503 0           $fields = "page_namespace, page_title";
504             }
505             else
506             {
507 0           $fields = "COUNT(*)";
508             }
509              
510 0           my $sql = "SELECT $fields FROM page";
511              
512 0           my $nodes = _utf8_on_array $dbh->selectall_arrayref ($sql);
513              
514 0 0         return $nodes->[0]->[0] unless wantarray;
515              
516 0           return map {
517 0           $self->__num_to_namespace ($_->[0], $_->[1])
518             } @$nodes;
519             }
520             }
521              
522              
523              
524             =head2 list_recent_changes
525              
526             Like the parent method, but the C argument may be used in conjunction
527             with the others (C, C, and C are still mutually
528             exclusive). A new, $args{between_secs} argument is also processed. Its
529             contents should be two unix timestamps.
530              
531             =cut
532              
533             sub list_recent_changes
534             {
535 0     0 1   my $self = shift;
536 0           my %args = @_;
537              
538 0           my $exclusive = 0;
539 0           foreach my $option (qw{days since between_days between_secs})
540             {
541 0 0         $exclusive++ if $args{$option};
542             }
543 0 0         croak "between_days, days, between_secs, & since options are "
544             . "mutually exclusive"
545             if $exclusive > 1;
546              
547 0 0         $args{between_days} = [delete $args{days}, 0]
548             if $args{days};
549              
550 0 0         if ($args{between_days})
551             {
552 0           croak "two arguments required for between_days"
553 0 0         unless @{$args{between_days}} == 2;
554              
555 0           my $now = gmtime;
556 0           $args{between_secs} = [map {$now - $_ * ONE_DAY}
  0            
557 0           @{$args{between_days}}];
558 0           delete $args{between_days};
559             }
560              
561 0 0         $args{between_secs} = [delete $args{since}, gmtime]
562             if $args{since};
563              
564 0 0         if ($args{between_secs})
565             {
566 0           croak "two arguments required for between_secs"
567 0 0         unless @{$args{between_secs}} == 2;
568 0           $args{between_secs} = [map {scalar gmtime $_}
  0            
569 0           sort { $a <=> $b }
570 0           @{$args{between_secs}}];
571             }
572              
573 0 0         $args{limit} = delete $args{last_n_changes}
574             if $args{last_n_changes};
575              
576 0           return $self->_find_recent_changes_by_criteria (%args);
577             }
578              
579              
580              
581             sub _get_metadata_sql
582             {
583 0     0     my ($self, $is, $table_prefix, $metadata, %args) = @_;
584 0           my $sql;
585              
586 0           my ($cmp, $in);
587              
588 0 0         if ($is)
589             {
590 0           $cmp = "=";
591 0           $in = "IN";
592             }
593             else
594             {
595 0           $cmp = "!=";
596 0           $in = "NOT IN";
597             }
598              
599 0           foreach my $key (keys %$metadata)
600             {
601 0 0         if ($key eq "edit_type")
    0          
    0          
    0          
602             {
603 0 0         if ($metadata->{$key} eq "Minor tidying")
    0          
604             {
605 0 0         if ($table_prefix eq "rc_")
    0          
606             {
607 0           $sql .= " AND rc_minor $cmp 1"
608             }
609             elsif ($table_prefix eq "rev_")
610             {
611 0           $sql .= " AND " . $table_prefix . "minor_edit $cmp 1"
612             }
613             }
614             elsif ($metadata->{$key} eq "Normal edit")
615             {
616 0 0         if ($table_prefix eq "rc_")
    0          
617             {
618 0           $sql .= " AND rc_minor $cmp 0"
619             }
620             elsif ($table_prefix eq "rev_")
621             {
622 0           $sql .= " AND " . $table_prefix . "minor_edit $cmp 0"
623             }
624             }
625             else
626             {
627 0           confess "unrecognized edit_type: `" . $metadata->{$key} . "'";
628             }
629             }
630             elsif ($key eq "username")
631             {
632 0 0         $sql .= " AND " . ($is ? "" : "NOT ")
633             . $self->_get_cmp_sql ($table_prefix . "user_text",
634             $metadata->{$key},
635             $args{ignore_case});
636             }
637             elsif ($key eq "patrolled")
638             {
639 0 0 0       if($table_prefix eq "rc_")
    0 0        
      0        
640             {
641 0           $sql .= " AND rc_patrolled $cmp " . $metadata->{$key};
642             }
643             elsif ($metadata->{$key} && $cmp eq '!='
644             || !$metadata->{$key} && $cmp eq '=')
645             {
646             # Assume patrolled is true for tables which don't store it.
647 0           $sql .= " AND 0 = 1";
648             }
649             }
650             elsif ($key eq "namespace")
651             {
652 0 0 0       if (reftype ($metadata->{$key})
653             && reftype ($metadata->{$key}) eq 'ARRAY')
654             {
655 0           croak "Namespace specification must be numeric"
656 0 0         if grep {!/^\d+$/} @{$metadata->{$key}};
  0            
657 0           $sql .= " AND $table_prefix" . "namespace $in ("
658 0           . join (", ", @{$metadata->{$key}}) . ")";
659             }
660             else
661             {
662 0 0         croak "Namespace specification must be numeric"
663             unless $metadata->{$key} =~ /^\d+$/;
664 0           $sql .= " AND $table_prefix" . "namespace $cmp "
665             . $metadata->{$key};
666             }
667             }
668             else
669             {
670 0           confess "unimplemented metadata key: `$key'";
671             }
672             }
673              
674 0           return $sql;
675             }
676              
677              
678              
679             sub _get_lim_off_sql
680             {
681 0     0     my (%args) = @_;
682              
683 0 0         if (exists $args{limit})
684             {
685 0 0 0       croak "Bad argument limit=`$args{limit}'"
686             unless defined $args{limit} && $args{limit} =~ /^\d+$/;
687             }
688 0 0         if (exists $args{offset})
689             {
690 0 0 0       croak "Bad argument offset=`$args{offset}'"
691             unless defined $args{offset} && $args{offset} =~ /^\d+$/;
692              
693             # This number is big.
694 0 0         $args{limit} = 18446744073709551615 unless defined $args{limit};
695             }
696              
697 0 0         return (defined $args{limit} ? " LIMIT $args{limit}" : "")
    0          
698             . ($args{offset} ? " OFFSET $args{offset}" : "");
699             }
700              
701              
702              
703             sub _build_where_sql
704             {
705 0     0     my ($self, $table_prefix, $metadata_is, $metadata_isnt, %args) = @_;
706 0 0         my $page_prefix = $table_prefix eq "rev_" ? "page_" : $table_prefix;
707              
708             # Initialize the clause.
709 0           my $wheresql = "WHERE 1 = 1";
710              
711 0 0         if ($args{name})
712             {
713 0           my ($ns, $node) = $self->__namespace_to_num ($args{name});
714 0           $wheresql .= " AND " . $page_prefix . "namespace = $ns"
715             . " AND "
716             . $self->_get_cmp_sql ($page_prefix . "title",
717             $node,
718             $args{ignore_case});
719             #Supply moderation => 1 if you only want to see versions that are moderated.
720 0 0         $wheresql .= " AND patrolled = 1"
721             if $args{moderation} == 1;
722             }
723              
724             # Set the start and finish timestamp to search between.
725 0           my ($s, $f);
726 0 0         if ($args{between_secs})
727             {
728             # This function assumes that it was called via recent_changes, which
729             # sorts the @{$args{between_secs}} array.
730 0 0         ($s, $f) = map {defined $_ ? ($_->strftime ($timestamp_fmt)) : $_}
  0            
731 0           @{$args{between_secs}};
732             }
733              
734 0 0         $wheresql .= " AND " . $table_prefix . "timestamp >= $s"
735             if $s;
736 0 0         $wheresql .= " AND " . $table_prefix . "timestamp <= $f"
737             if $f;
738              
739 0 0         $wheresql .= $self->_get_metadata_sql (1, $table_prefix, $metadata_is, %args)
740             if $metadata_is;
741 0 0         $wheresql .= $self->_get_metadata_sql (0, $table_prefix, $metadata_isnt, %args)
742             if $metadata_isnt;
743              
744             # Hide Log/Delete entries in RC
745 0 0         $wheresql .= " AND " . $page_prefix . "title != 'Log/Delete'"
746             if $args{hidedelete};
747              
748 0           return $wheresql;
749             }
750              
751              
752             =head2 list_unmoderated_nodes
753              
754             $store->list_unmoderated_nodes (only_where_latest => 0);
755              
756             Like the L function of the same name, returns
757             the list of nodes which have not been moderated (in Mediawiki context, this
758             is the list of nodes that have revisions that have not had their "patrolled"
759             bit set).
760              
761             C defaults to 0 and, when set, returns revisions iff they
762             are both the most recent revision of a node and remain unmoderated. i.e.,
763             there will be at most one entry returned per node and a node with a moderated
764             latest edit but which has older, unmoderated edits, will not appear in the
765             list.
766              
767             =cut
768              
769             sub list_unmoderated_nodes
770             {
771 0     0 1   my ($self, %args) = @_;
772              
773 0           $args{include_all_changes} = !$args{only_where_latest};
774 0           $args{metadata_isnt} = "patrolled";
775 0           return $self->_find_recent_changes_by_criteria (%args);
776             }
777              
778             sub _find_recent_changes_by_criteria
779             {
780 0     0     my ($self, %args) = @_;
781 0           my ($since, $between_days, $include_all_changes,
782             $metadata_is, $metadata_isnt, $metadata_was, $metadata_wasnt) =
783             @args{qw(since between_days include_all_changes
784             metadata_is metadata_isnt metadata_was metadata_wasnt)};
785 0           my $dbh = $self->dbh;
786 0           my $infields;
787             my @outfields;
788 0 0         my $ignore_case = exists $args{ignore_case}
789             ? $args{ignore_case} : $self->{ignore_case};
790              
791 0 0         my ($ns, $name) = $self->__namespace_to_num ($args{name})
792             if $args{name};
793              
794              
795             # Don't know the rationale for this complex algorithm to determine which
796             # table to use, but I copied it from Wiki::Toolkit::Store::Database. It
797             # works out such that, in order, include_all_changes == 1 will always force
798             # the view including history. metadata_is and metadata_isnt will always be
799             # processed, history or no, but if either is set then metadata_was and
800             # metadata_wasnt are ignored. If neither metadata_is and metadata_isnt are
801             # set, and either metadata_was or metadata_wasnt are set, then the view
802             # including history is selected, regardless of the value of
803             # include_all_changes.
804             #
805             # It seems to me like it would be easier to just accept two metadata
806             # arguments and let include_all_changes switch tables, but I am
807             # implementing this anyway for backwards compatibility.
808 0 0 0       unless ($metadata_is || $metadata_isnt)
809             {
810 0 0 0       $include_all_changes = 1
811             if $metadata_was || $metadata_wasnt;
812              
813 0           $metadata_is = $metadata_was;
814 0           $metadata_isnt = $metadata_wasnt;
815             }
816              
817             # Count the number of records that will be returned.
818 0           my ($rows, $sql);
819              
820             # This union of the recentchanges table and the revision table will be
821             # reused...
822 0           my $rcsql = "SELECT rc_this_oldid, rc_user_text, rc_comment, "
823             . "rc_timestamp, rc_minor, rc_namespace, rc_title "
824             . "FROM recentchanges "
825             . $self->_build_where_sql ("rc_", $metadata_is, $metadata_isnt,
826             %args);
827              
828 0           my $useOld;
829 0 0         if (wantarray)
830             {
831             # Count the number of records that will be returned.
832 0           my $rcCount;
833 0           $sql = "SELECT ";
834 0           $sql .= "COUNT(*) FROM recentchanges ";
835 0           $sql .= $self->_build_where_sql ("rc_", $metadata_is, $metadata_isnt,
836             %args);
837 0 0         $sql .= " GROUP BY rc_namespace, rc_title"
838             unless $include_all_changes;
839              
840 0           $rows = _utf8_on_array $dbh->selectall_arrayref ($sql);
841 0           $rcCount = $rows->[0]->[0];
842              
843             # Decide whether we need more rows than are available in recentchanges.
844 0 0         $useOld = 1
    0          
    0          
845             if (defined $args{limit} ? $args{limit} : 0)
846             + (defined $args{offset} ? $args{offset} : 0) > $rcCount;
847             }
848             else # !wantarray
849             {
850             # In the !wantarray case, offset and limit are ignored and we always
851             # need to count total records available from both tables.
852 0           $useOld = 1;
853             }
854              
855 0           my $basesql;
856 0 0         if ($useOld)
857             {
858             # In the $useOld case, the revision table needs to be joined with recent
859             # changes. Even though all recentchanges exist in the revisions table,
860             # delete log entries and moves do not. Duplicates are removed via the
861             # UNION DISTINCT SQL operator.
862 0           my $revsql = "SELECT rev_text_id, rev_user_text, rev_comment, "
863             . "rev_timestamp, rev_minor_edit, "
864             . "page_namespace AS rev_namespace, "
865             . "page_title AS rev_title "
866             . "FROM revision JOIN page ON page_id=rev_page "
867             . $self->_build_where_sql ("rev_", $metadata_is,
868             $metadata_isnt, %args);
869              
870 0           $basesql = "($rcsql) UNION ($revsql)";
871              
872             }
873             else # !$useOld (we can get what we want from recentchanges)
874             {
875 0           $basesql = $rcsql;
876             }
877              
878             # We don't care what order things come out in when we are only counting.
879 0 0         $basesql .= " ORDER BY rc_timestamp DESC"
880             if wantarray;
881              
882 0 0         unless ($include_all_changes)
883             {
884 0           $basesql = "SELECT * FROM ($basesql) AS r "
885             . "GROUP BY rc_namespace, rc_title";
886              
887             # We don't care what order things come out in when we are only counting.
888 0 0         $basesql .= " ORDER BY rc_timestamp DESC"
889             if wantarray;
890             }
891              
892             # Decide what fields will need to be retrieved from the DB.
893 0           my $tables;
894 0 0         if (wantarray)
895             {
896             # No need to merge the patrolled flag if all we want is a count.
897 0           my $patrolledsql = "SELECT rc_this_oldid, rc_new, rc_patrolled "
898             . "FROM recentchanges "
899             . "WHERE rc_this_oldid > 0";
900              
901 0           $tables = "($basesql) AS b NATURAL LEFT JOIN ($patrolledsql) AS p";
902              
903 0           $infields = join ", ", qw{rc_this_oldid rc_user_text rc_comment
904             rc_timestamp rc_minor rc_namespace rc_title
905             rc_new rc_patrolled};
906 0           @outfields = qw{version username comment last_modified edit_type
907             ns name is_new patrolled};
908             }
909             else
910             {
911 0           $tables = "($basesql) AS b";
912 0           $infields = "COUNT(*)";
913             }
914              
915 0           $sql = "SELECT $infields FROM $tables";
916              
917 0 0         if (wantarray)
918             {
919             # A final GROUP BY clause in the !wantarray case converts the
920             # COUNT(*) into an agregate function counting each row once
921             # and is unnecessary without the patrolled merge.
922 0 0         $sql .= " GROUP BY rc_namespace, rc_title"
923             unless $include_all_changes;
924              
925             # No need to specify order when we are returning a count.
926 0           $sql .= " ORDER BY rc_timestamp DESC";
927              
928             # limit and offset are ignored when returning a count.
929 0           my $limoffsql = _get_lim_off_sql (%args);
930 0 0         $sql .= $limoffsql if $limoffsql;
931             }
932            
933 0           my $nodes = _utf8_on_array $dbh->selectall_arrayref ($sql);
934              
935 0 0         return $nodes->[0]->[0] unless wantarray;
936              
937 0           my @newnodes;
938 0           foreach my $i (0 .. (@$nodes - 1))
939             {
940 0           my %node;
941 0           @node{@outfields} = @{$nodes->[$i]};
  0            
942 0           $node{name} =
943             $self->__num_to_namespace ($node{ns},
944             $node{name});
945 0 0         $node{edit_type} = $node{edit_type} ? "Minor tidying" : "Normal edit";
946 0           $node{last_modified} = $self->_make_date ($node{last_modified});
947 0           _make_metadata \%node;
948 0           push @newnodes, \%node;
949             }
950 0           return @newnodes;
951             }
952              
953              
954             =head2 set_node_moderation
955              
956             This method's concept has no parallel in Mediawiki.
957              
958             =cut
959              
960             sub set_node_moderation {
961 0     0 1   croak "Unimplemented set_node_moderation, see Wiki::Toolkit::Store::Mediawiki documentation for details.";
962             }
963              
964              
965             =head2 moderate_node
966              
967             $store->moderate_node (version => $version);
968              
969             Give a version number (rc_this_oldid from recent changes), mark it as
970             patrolled. If the revisions no longer exists in the recent changes table,
971             silently ignore this.
972              
973             =cut
974              
975             sub moderate_node {
976 0     0 1   my ($self, %args) = @_;
977 0           my $dbh = $self->dbh;
978              
979 0 0 0       croak "version argument `$args{version}' is not numeric."
980             unless defined $args{version} && $args{version} =~ /^\d+$/;
981              
982 0           my $sql = "UPDATE recentchanges SET rc_patrolled = 1 WHERE"
983             . " rc_this_oldid = " . $dbh->quote ($args{version});
984 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
985             }#end moderate_node
986              
987             =head2 set_node_restrictions
988              
989             $store->set_node_restrictions (name => $nodename, username => $username, set => %restrictions, %otherargs);
990             WHERE
991             %restrictions is of the form $restriction{restrictionType} = @affectedGroups;
992              
993             Requires a node name or page id, and at least one set restriction argument.
994             The method will add or remove the permissions for the specified user groups
995             to the 'page_restriction' field for the page corresponding to
996             the node name given.
997              
998             =cut
999              
1000             sub set_node_restrictions {
1001 0     0 1   my ($self, %args) = @_;
1002 0 0         my ($ns, $name) = $self->__namespace_to_num ($args{name})
1003             if $args{name};
1004 0           my $dbh = $self->dbh;
1005 0           my $where = "";
1006              
1007 0 0 0       croak "Only one `id` or `name` argument required."
1008             if $args{name} && $args{id};
1009 0 0 0       croak "At least one set or remove restriction is required"
1010             unless $args{set} || $args{remove};
1011              
1012             #set up where for id or name
1013 0 0         $where = " WHERE page_id = " . $dbh->quote($args{id})
1014             if $args{id};
1015 0 0         $where = " WHERE page_namespace = " . $ns
1016             . " AND page_title = " . $dbh->quote($name)
1017             if $name;
1018              
1019             #set us up the restrictions string.
1020 0           my $res_string = 'edit=autoconfirmed:';
1021 0 0         $res_string = 'edit=registered:'
1022             if $args{set}->{edit} eq 'registered';
1023 0 0         $res_string = 'edit=sysop:'
1024             if $args{set}->{edit} eq 'sysop';
1025              
1026 0 0         $res_string .= 'move=registered'
1027             if $args{set}{move} eq 'registered';
1028 0 0         $res_string .= 'move=sysop'
1029             if $args{set}{move} eq 'sysop';
1030 0 0         $res_string .= 'move=autoconfirmed'
1031             if $args{set}{move} eq 'default';
1032            
1033             #update page restrictions
1034 0 0         my $sql = "UPDATE page SET page_restrictions = "
1035             . ($res_string ne '' ? $dbh->quote($res_string) : 'NULL') . $where;
1036              
1037 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1038              
1039             #set up action
1040 0           my $action = 'protect';
1041 0 0 0       $action = 'unprotect'
1042             if ($args{set}{edit} eq 'default' && $args{set}{edit} eq 'default');
1043             #make a log entry
1044 0           $self->_make_log(name => $args{name}, type => 'protect',
1045             action => $action, username => $args{username},
1046             comment => $args{comment}."($res_string)",
1047             params => $args{edit_type}, ip => $args{ip});
1048             }#end set_node_restrictions
1049              
1050              
1051             =head2 delete_node
1052              
1053             This method is unimplemented due to lack of support for archival
1054             and logging of deleted pages from Wiki::Toolkit.
1055              
1056             Please see the C documentation for the deletetion of nodes as
1057             is comparable to the Mediawiki method of node removal.
1058              
1059             =cut
1060              
1061             sub delete_node {
1062 0     0 1   croak "Unimplemented delete_node, see Wiki::Toolkit::Store::Mediawiki documentation for details.";
1063             }
1064              
1065              
1066             =head2 delete_page
1067              
1068             $store->delete_page ($name, $comment, $edit_type, $username, $ip);
1069             OR
1070             $store->delete_page ($name, $comment, $edit_type, $username, $ip, $version);
1071              
1072             Given the node name, a comment about deletion, user name, and user IP this
1073             will 'delete' a page and its history from the wiki. If also given a version
1074             number, only the specified revision will be removed.
1075              
1076             This moves all or specified revisions of a page to the archive table, removes
1077             all related rows from recentchanges/page/revision, and adds a row to
1078             recentchanges noting the deletion.
1079              
1080             =cut
1081              
1082             sub delete_page
1083             {
1084             #here the name is the ... page title, eat and delete
1085 0     0 1   my ($self,%args) = @_;
1086 0           my ($ns, $name) = $self->__namespace_to_num ($args{name});
1087 0           my $timestamp = $self->_get_timestamp ();
1088 0           my $dbh = $self->dbh;
1089 0           my $where;
1090 0           my $version = $args{version};
1091 0 0 0       croak "invalid version number"
1092             if $version && $version !~ /^\d+$/; #non numeric version supplied
1093 0           my $sql = "SELECT user_id, user_name FROM user WHERE "
1094             . $self->_get_cmp_sql ("user_name",
1095             $args{username},
1096             $args{ignore_case});
1097 0           my $userId = _utf8_on_array $dbh->selectrow_arrayref ($sql);
1098 0           my $newusername = $userId->[1];
1099              
1100             #get page_id of what we are 'deleting', if we aren't tossing all, we may need to update latest
1101 0           $sql = "SELECT page_id FROM page"
1102             . " WHERE page_namespace = " . $ns
1103             . " AND "
1104             . $self->_get_cmp_sql ("page_title", $name);
1105              
1106 0           my $pageId = _utf8_on_array $dbh->selectrow_arrayref ($sql);
1107 0           $pageId = $pageId->[0];
1108              
1109             #move stuff to archive
1110 0           $sql = " INSERT INTO archive (ar_namespace, ar_title, ar_comment, ar_user, ar_user_text,"
1111             . " ar_timestamp, ar_minor_edit, ar_rev_id, ar_text_id)"
1112             . " SELECT page_namespace, page_title, rev_comment, rev_user, rev_user_text,"
1113             . " rev_timestamp, rev_minor_edit, rev_id, rev_text_id"
1114             . " FROM revision JOIN page ON rev_page = page_id";
1115 0           $where = " WHERE page_id = " . $pageId;
1116            
1117 0 0         $where .= " AND rev_text_id = $version"
1118             if $version;
1119              
1120 0           $sql .= $where;
1121 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1122              
1123             #remove from recent changes
1124 0           $sql = "DELETE FROM recentchanges WHERE rc_cur_id = " . $pageId;
1125 0 0         $sql = "DELETE FROM recentchanges WHERE rc_this_oldid = $version"
1126             if $version;
1127 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1128             #remove from revision
1129 0           $sql = "DELETE FROM revision WHERE rev_page = " . $pageId;
1130 0 0         $sql = "DELETE FROM revision WHERE rev_text_id = " . $version
1131             if ($version);
1132 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1133             #remove from page
1134 0           $sql = "DELETE FROM page WHERE page_id = " . $pageId;
1135              
1136             #get new page latest if there is one
1137 0           $sql = "SELECT rev_text_id FROM revision WHERE rev_page = " . $pageId
1138             . " ORDER BY rev_timestamp DESC LIMIT 1";
1139 0           my $latest = _utf8_on_array $dbh->selectrow_arrayref ($sql);
1140 0           $latest = $latest->[0];
1141              
1142             #get new page length
1143 0 0         if ($latest) {
1144 0           $sql = "SELECT LENGTH(old_text) FROM text WHERE old_id = " . $latest;
1145 0           my $length = _utf8_on_array $dbh->selectrow_arrayref ($sql);
1146 0           $length = $length->[0];
1147              
1148             #set new page latest and length
1149 0           $sql = "UPDATE page SET page_latest = ". $latest . ","
1150             . " page_len =" . $length
1151             . " WHERE page_id = $pageId";
1152             }
1153 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1154              
1155             #remove links if whole page removed.
1156 0 0         unless ($latest){
1157 0 0         $dbh->do ("DELETE FROM pagelinks WHERE pl_from = " . $pageId)
1158             or croak $dbh->errstr;
1159 0 0         $dbh->do ("DELETE FROM templatelinks WHERE tl_from = " . $pageId)
1160             or croak $dbh->errstr;
1161 0 0         $dbh->do ("DELETE FROM externallinks WHERE el_from = " . $pageId)
1162             or croak $dbh->errstr;
1163             }
1164              
1165             #make a recent changes log entry
1166 0           $self->_make_log(name => $args{name}, type => 'delete', action => 'delete', username => $newusername,
1167             comment => $args{comment}, params => $args{edit_type}, ip => $args{ip});
1168             }#end delete_page
1169              
1170              
1171             =head2 restore_page
1172              
1173             $store->restore_page (name => $name, revisions => \@revisions,
1174             username => $username, ip => $ip);
1175              
1176              
1177             Given the node name, this will restore all versions of a 'deleted' wiki page.
1178             If given a version number or numbers, it will restore all 'deleted' revisions
1179             selected.
1180              
1181             If a new page with the same name has been created since the last delete, the
1182             revisions will be restored to history, but the new most recent page will not
1183             change.
1184              
1185             This move revisions of a page from archive and repopulates revision/page
1186             with the appropriate data. It then adds a log entry into recentchanges
1187             to denote that there was a restoration.
1188              
1189             =cut
1190             sub restore_page {
1191 0     0 1   my ($self,%args) = @_;
1192 0           my ($node) = $args{name};
1193 0           my ($ns, $name) = $self->__namespace_to_num ($node);
1194 0           my $timestamp = $self->_get_timestamp ();
1195 0           my $dbh = $self->dbh;
1196              
1197 0           my @revisions;
1198 0 0         if ($args{revisions}) {
1199 0           @revisions = @{$args{revisions}};
  0            
1200 0           my @wrong = grep !/^\d+$/, @revisions;
1201 0 0         croak "choking on non-numeric revision" . (@revisions > 1 ? "s" : "")
    0          
1202             . ": ", join (",", @wrong) . "."
1203             if @wrong;
1204             }
1205              
1206 0           my $pageId;
1207             my $newPageId;
1208 0           my $where;
1209 0           my $sql = "SELECT user_id, user_name FROM user WHERE "
1210             . $self->_get_cmp_sql ("user_name",
1211             $args{username},
1212             $args{ignore_case});
1213 0           my $userId = $dbh->selectrow_arrayref ($sql);
1214 0           my $newusername = $userId->[1];
1215 0           $userId = $userId->[0];
1216              
1217             #get page_id of what we are 'restoring'.
1218 0           $sql = "SELECT page_id FROM page"
1219             . " WHERE page_namespace = " . $ns
1220             . " AND "
1221             . $self->_get_cmp_sql ("page_title", $name);
1222              
1223 0           $pageId = _utf8_on_array $dbh->selectrow_arrayref ($sql);
1224 0           $pageId = $pageId->[0];
1225              
1226 0 0         unless ($pageId) {# we'll have to update the page length and latest after.
1227 0           $sql = "INSERT INTO page (page_namespace, page_title, page_touched, "
1228             . "page_counter, page_is_redirect, "
1229             . "page_is_new, page_random, page_latest, page_restrictions)"
1230             . " VALUES ($ns, "
1231             . $dbh->quote ($name) . ", "
1232             . $dbh->quote ($timestamp)
1233             . ", 0, 0, 1, 0, 0, 'autoconfirmed')";
1234 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1235              
1236             #get newly inserted page_id of what we are 'restoring'.
1237 0           $sql = "SELECT page_id FROM page"
1238             . " WHERE page_namespace = " . $ns
1239             . " AND "
1240             . $self->_get_cmp_sql ("page_title", $name);
1241              
1242 0           $newPageId = _utf8_on_array $dbh->selectrow_arrayref ($sql);
1243 0           $pageId = $newPageId->[0];
1244             }
1245              
1246             #move stuff to revision.
1247 0           $sql = "INSERT INTO revision (rev_id, rev_page, rev_comment, rev_user, rev_user_text,"
1248             . " rev_timestamp, rev_minor_edit, rev_deleted, rev_text_id)"
1249             . " SELECT ar_rev_id, $pageId, ar_comment, ar_user, ar_user_text,"
1250             . " ar_timestamp, ar_minor_edit, 0, ar_text_id"
1251             . " FROM archive";
1252 0 0         if (@revisions > 0) { #Either a specific list of revisions, or all of that page.
1253 0           $where = " WHERE ar_text_id IN (" . join(", ", @revisions) . ")";
1254             } else {
1255 0           $where = " WHERE ar_namespace = $ns "
1256             . " AND ar_title = ". $dbh->quote($name);
1257             }
1258 0           $sql .= $where;
1259 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1260              
1261             #remove restored revisions from archive.
1262 0           $sql = "DELETE FROM archive" . $where;
1263 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1264              
1265             #update page, it doesn't matter if the restored version used to be the latest
1266             #and now there is a new page in place, the version numbers sort themselves correctly.
1267             #get new page latest
1268 0           $sql = "SELECT rev_text_id FROM revision WHERE rev_page = "
1269             . $pageId
1270             . " ORDER BY rev_timestamp DESC LIMIT 1";
1271 0           my $latest = _utf8_on_array $dbh->selectrow_arrayref ($sql);
1272 0           $latest = $latest->[0];
1273              
1274             #get new page length
1275 0           $sql = "SELECT LENGTH(old_text) FROM text WHERE old_id = "
1276             . $latest;
1277 0           my $length = _utf8_on_array $dbh->selectrow_arrayref ($sql);
1278 0           $length = $length->[0];
1279              
1280             #set new page latest and length
1281 0           $sql = "UPDATE page SET page_latest = ". $latest . ","
1282             . " page_len =" . $length
1283             . " WHERE page_id = $pageId";
1284 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1285            
1286             #make log entry for restoration
1287 0           my $comment = "restored \"[[" . $node;
1288              
1289 0 0         if (@revisions > 0) {
1290 0           $comment .= "]]\": ". (scalar @revisions) . " revisions restored";
1291             } else {
1292 0           $comment .= "]]\": All revisions restored";
1293             }
1294 0           $self->_make_log(name => $args{name}, type => 'delete', action => 'restore', username => $newusername,
1295             comment => $comment, params => $args{edit_type}, ip => $args{ip});
1296             }#end restore_page
1297              
1298              
1299              
1300             =head2 list_archived_pages
1301              
1302             $store->list_archived_pages (name => $name);
1303              
1304             Loads and returns the list of deleted pages from the archive table.
1305              
1306             =cut
1307              
1308             sub list_archived_pages {
1309 0     0 1   my ($self, %args) = @_;
1310 0           my $dbh = $self->dbh;
1311 0           my @outfields = qw{ns name comment userid username last_modified edit_type version text_id};
1312 0           my ($ns, $name) = $self->__namespace_to_num ($args{name});
1313            
1314 0           my $sql;
1315 0 0         if (wantarray){
1316 0           $sql = "SELECT ar_namespace, ar_title, ar_comment, ar_user,"
1317             . " ar_user_text, ar_timestamp ar_minor_edit, ar_rev_id, ar_text_id";
1318             } else {
1319 0           $sql = "SELECT COUNT(*)";
1320             }
1321 0           $sql .= " FROM archive"
1322             . " WHERE ar_namespace = " . $ns
1323             . " AND ar_title = " . $dbh->quote($name)
1324             . " ORDER BY ar_timestamp DESC";
1325              
1326 0           my $limoffsql = _get_lim_off_sql (%args);
1327 0 0 0       $sql .= $limoffsql if ($limoffsql && wantarray);
1328              
1329 0           my $nodes = _utf8_on_array $dbh->selectall_arrayref ($sql);
1330              
1331 0 0         return $nodes->[0]->[0]
1332             unless wantarray;
1333              
1334 0           my @newnodes;
1335 0           foreach my $i (0 .. (@$nodes - 1))
1336             {
1337 0           my %node;
1338 0           @node{@outfields} = @{$nodes->[$i]};
  0            
1339 0           $node{name} = $self->__num_to_namespace ($node{ns}, $node{name});
1340 0 0         $node{edit_type} = $node{edit_type} ? "Minor tidying" : "Normal edit";
1341 0           $node{last_modified} = $self->_make_date ($node{last_modified});
1342 0           _make_metadata \%node;
1343 0           push @newnodes, \%node;
1344             }
1345 0           return @newnodes;
1346              
1347             }#end list_archived_pages
1348              
1349              
1350             # $self->_get_cmp_sql (FIELD, TEXT, IGNORE_CASE)
1351             # Return text that would return TRUE in a DB query's WHERE clause, if
1352             # the contents of FIELD matches TEXT, honoring first IGNORE_CASE, then
1353             # defaulting to $self->{ignore_case} when IGNORE_CASE is undefined.
1354             sub _get_cmp_sql
1355             {
1356 0     0     my ($self, $field, $name, $ignore_case) = @_;
1357 0 0         $ignore_case = $self->{ignore_case} unless defined $ignore_case;
1358 0           my $dbh = $self->{_dbh};
1359              
1360             # The MySQL documentation says that comparison using like should default
1361             # to a case insensitive comparison, but for some reason this isn't
1362             # happening by default. Force it instead using the COLLATE keyword.
1363 0 0         if ($ignore_case)
1364             {
1365 0           $name =~ s/%/\\%/g;
1366 0           my $charset;
1367 0 0         $charset = "utf8"
1368             if $self->{_charset}=~/^utf-?8$/i;
1369 0 0         $charset = "latin1"
1370             if $self->{_charset}=~/^ISO-8859-1$/i;
1371            
1372 0           return "$field LIKE " . $dbh->quote($name)
1373             . " COLLATE " . $charset . "_general_ci";
1374             }
1375              
1376 0           return "$field = " . $dbh->quote($name);
1377             }
1378              
1379             # $store->_make_log($node_name, $log_type, $log_action, $log_user, $log_comment, $log_params)
1380             # make a log entry into logging table, and a recent changes entry denoting the log took place
1381             # log_types are delete | move | protect
1382             # log_actions are delete,restore | move | protect,unprotect
1383             sub _make_log {
1384 0     0     my ($self, %args) = @_;
1385 0           my $dbh = $self->dbh;
1386 0           my ($ns, $name) = $self->__namespace_to_num ($args{name});
1387 0           my $timestamp = $self->_get_timestamp ();
1388 0           my $where;
1389              
1390 0           my $sql = "SELECT user_id, user_name FROM user WHERE "
1391             . $self->_get_cmp_sql ("user_name",
1392             $args{username},
1393             $args{ignore_case});
1394 0           my $userId = _utf8_on_array $dbh->selectrow_arrayref ($sql);
1395 0           my $newusername = $userId->[1];
1396 0           $userId = $userId->[0];
1397            
1398 0           my $type = "Log/";
1399              
1400 0 0         $type .= "Delete" if $args{type} eq 'delete';
1401 0 0         $type .= "Protect" if $args{type} eq 'protect';
1402 0 0         $type .= "Move" if $args{type} eq 'move';
1403              
1404             #make a logging entry
1405 0 0         $sql = "INSERT INTO logging (log_type, log_action, log_timestamp,"
1406             . " log_user, log_namespace, log_title, log_comment, log_params)"
1407             . " VALUES (". $dbh->quote($args{type}) . ", " . $dbh->quote($args{action}) . ", " . $timestamp
1408             . ", " . $dbh->quote($userId). ", " . $ns . ", " . $dbh->quote($name)
1409             . ", " . $dbh->quote($args{comment})
1410             . ", " . ($args{params} ? $dbh->quote($args{params}) : "''") .")";
1411              
1412 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1413              
1414             #make a recent changes log entry
1415 0 0         $sql = "INSERT INTO recentchanges (rc_timestamp, rc_cur_time, rc_user, "
1416             . "rc_user_text, rc_namespace, rc_title, "
1417             . "rc_comment, rc_minor, rc_bot, rc_new, "
1418             . "rc_cur_id, rc_this_oldid, "
1419             . "rc_last_oldid, rc_type, rc_moved_to_ns, "
1420             . "rc_moved_to_title, rc_patrolled, rc_ip) "
1421             . "VALUES ($timestamp, $timestamp, $userId, "
1422             . $dbh->quote ($newusername)
1423             . ", -1, ". $dbh->quote($type) .", "
1424             . $dbh->quote ($args{comment})
1425             . ", "
1426             . ($args{param} eq 'Minor tidying' ? 1 : 0)
1427             . ", 0, 0, 0, 0, 0, 3, 0, '', 1, "
1428             . $dbh->quote ($args{ip})
1429             . ")";
1430              
1431 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1432             }
1433              
1434             # $store->_get_relative_version ($node_name, $node_version, $direction);
1435             # Return the version number of the previous or next node, as specified.
1436             sub _get_relative_version
1437             {
1438 0     0     my ($self) = shift;
1439              
1440 0           my ($direction, $node, $version) = @_[0 .. 2];
1441 0 0         croak "version `$version' is not a number"
1442             unless $version =~ /^\d+$/;
1443              
1444 0 0         my %args = @_[3 .. $#_] if @_ > 3;
1445              
1446 0           my ($ns, $name) = $self->__namespace_to_num ($node);
1447 0           my $dbh = $self->dbh;
1448 0           my $sql = "SELECT rc_this_oldid FROM"
1449             . " (SELECT * FROM"
1450             . " (SELECT rc_this_oldid, rc_namespace, rc_title FROM recentchanges) as rc"
1451             . " UNION ALL"
1452             . " SELECT * FROM"
1453             . " (SELECT rev_text_id, page_namespace, page_title FROM revision"
1454             . " JOIN page ON page_id=rev_page) as revPage) as history"
1455             . " WHERE rc_namespace = $ns"
1456             . " AND "
1457             . $self->_get_cmp_sql ("rc_title", $name,
1458             $args{ignore_case})
1459             . " AND rc_this_oldid $direction $version"
1460             . " ORDER BY rc_this_oldid";
1461              
1462 0 0         $sql .= " DESC" if $direction eq '<';
1463 0           $sql .= " LIMIT 1";
1464              
1465 0           my $ver = _utf8_on_array $dbh->selectrow_arrayref ($sql);
1466 0           return $ver->[0];
1467             }
1468              
1469              
1470              
1471             =head2 get_previous_version
1472              
1473             $store->get_previous_version ($node_name, $node_version, %other_args);
1474              
1475             Given a version number, returns the previous version for the given node.
1476             This function is necessary because mediawiki gives every revision of every
1477             page a version number which is unique across all pages.
1478              
1479             Techincally, node name shouldn't be necessary here, but it allows for a faster
1480             search and you probably have it. Not requiring it would be an easy hack.
1481              
1482             =cut
1483              
1484             sub get_previous_version
1485             {
1486 0     0 1   my $self = shift;
1487 0           return $self->_get_relative_version ('<', @_);
1488             }
1489              
1490              
1491              
1492             =head2 get_next_version
1493              
1494             $store->get_next_version ($node_name, $node_version, %other_args);
1495              
1496             Given a version number, returns the next version for the given node.
1497             This function is necessary because mediawiki gives every revision of every
1498             page a version number which is unique across all pages.
1499              
1500             Techincally, node name shouldn't be necessary here, but it allows for a faster
1501             search and you probably have it. Not requiring it would be an easy hack.
1502              
1503             =cut
1504              
1505             sub get_next_version
1506             {
1507 0     0 1   my $self = shift;
1508 0           return $self->_get_relative_version ('>', @_);
1509             }
1510              
1511              
1512              
1513             =head2 get_current_version
1514              
1515             $store->get_current_version ($node);
1516             $store->get_current_version (name => $node, %other_args);
1517              
1518             Given a node, returns the current (most recent) version, or undef, if the node
1519             does not exist.
1520              
1521             =cut
1522              
1523             sub get_current_version
1524             {
1525 0     0 1   my $self = shift;
1526 0           my %args;
1527              
1528 0 0         if (@_ == 1)
1529             {
1530 0           $args{name} = $_[0];
1531             }
1532             else
1533             {
1534 0           %args = @_;
1535             }
1536              
1537 0           my ($ns, $name) = $self->__namespace_to_num ($args{name});
1538 0           my $dbh = $self->dbh;
1539              
1540 0           my $sql = "SELECT page_latest FROM page"
1541             . " WHERE page_namespace = $ns"
1542             . " AND "
1543             . $self->_get_cmp_sql ("page_title",
1544             $name,
1545             $args{ignore_case});
1546 0           my $ver = _utf8_on_array $dbh->selectrow_arrayref ($sql);
1547 0 0         return $ver ? $ver->[0] : undef;
1548             }
1549              
1550              
1551             =head2 get_oldest_version
1552              
1553             $store->get_oldest_version ($node);
1554             $store->get_oldest_version (name => $node, %other_args);
1555              
1556             Given a node, returns the oldest (first non-archived) version, or undef, if the
1557             node does not exist.
1558              
1559             =cut
1560             sub get_oldest_version
1561             {
1562 0     0 1   my $self = shift;
1563 0           my %args;
1564              
1565 0 0         if (@_ == 1)
1566             {
1567 0           $args{name} = $_[0];
1568             }
1569             else
1570             {
1571 0           %args = @_;
1572             }
1573              
1574 0           my ($ns, $name) = $self->__namespace_to_num ($args{name});
1575 0           my $dbh = $self->dbh;
1576              
1577 0           my $sql = "SELECT rev_text_id FROM revision JOIN page on rev_page = page_id"
1578             . " WHERE page_namespace = $ns"
1579             . " AND "
1580             . $self->_get_cmp_sql ("page_title",
1581             $name,
1582             $args{ignore_case})
1583             . " LIMIT 1";
1584 0           my $ver = _utf8_on_array $dbh->selectrow_arrayref ($sql);
1585 0 0         return $ver ? $ver->[0] : undef;
1586             }
1587              
1588              
1589              
1590             sub _get_timestamp
1591             {
1592 0     0     my $self = shift;
1593             # I don't care about no steenkin' timezones (yet).
1594 0   0       my $time = shift || localtime; # Overloaded by Time::Piece::Adaptive.
1595             # Make it into an object for strftime
1596 0 0         $time = localtime $time unless ref $time;
1597 0           return $time->strftime ($timestamp_fmt); # global
1598             }
1599              
1600             =head2 update_links
1601              
1602             $config->{store}update_links( name => $node, links=> \@links_to );
1603            
1604             Given a node and a list containing internal, external, and template links,
1605             update the three link tables.
1606              
1607             =cut
1608             sub update_links
1609             {
1610 0     0 1   my ($self, %args) = @_;
1611 0           my ($node, $links_to) = @args{qw(name links)};
1612 0           my $dbh = $self->dbh;
1613 0           my $page_id;
1614             my $sql;
1615              
1616 0           my ($ns, $name) = $self->__namespace_to_num ($node);
1617              
1618              
1619 0           $sql = "SELECT page_id FROM page"
1620             . " WHERE page_namespace = " . $ns
1621             . " AND "
1622             . $self->_get_cmp_sql ("page_title", $name);
1623              
1624 0           $page_id = _utf8_on_array $dbh->selectrow_arrayref ($sql);
1625 0           $page_id = $page_id->[0];
1626              
1627             # Clear any old links for this page if it still exists
1628 0 0         if ($page_id){
1629 0 0         $dbh->do ("DELETE FROM pagelinks WHERE pl_from = ". $page_id)
1630             or croak $dbh->errstr;
1631 0 0         $dbh->do ("DELETE FROM externallinks WHERE el_from = ". $page_id)
1632             or croak $dbh->errstr;
1633 0 0         $dbh->do ("DELETE FROM templatelinks WHERE tl_from = ". $page_id)
1634             or croak $dbh->errstr;
1635              
1636 0           my $lastlink;
1637             my @locallinks;
1638 0           my @externallinks;
1639 0           my @templatelinks;
1640 0           foreach (@$links_to)
1641             {# Skip non-wtfmLink objects - we could try to sort with regex, but ambiguity is unavoidable.
1642 0 0         next unless $_->isa("Wiki::Toolkit::Formatter::Mediawiki::Link");
1643 0 0         if($_->{type} eq 'template')
    0          
    0          
1644 0           { push @templatelinks, $_->{name}; }
1645             elsif($_->{type} eq 'external')
1646 0           { push @externallinks, $_->{name}; }
1647             elsif($_->{type} eq 'page')
1648 0           { push @locallinks, $_->{name}; }
1649             }
1650              
1651             # Insert into the pagelinks table.
1652 0           $sql = "INSERT INTO pagelinks (pl_from, pl_namespace, pl_title)"
1653             . " VALUES ($page_id, ?, ?)";
1654 0 0         my $st1 = $dbh->prepare ($sql) or croak $dbh->errstr;
1655 0           foreach my $link (sort @locallinks)
1656             {
1657 0           my $en = ($link)[0];
1658 0           my ($ns, $t) = $self->__namespace_to_num ($en);
1659 0           $st1->execute ($ns, $t);
1660             }
1661 0           $st1->finish;
1662              
1663             # Insert into the templatelinks table.
1664 0           $sql = "INSERT INTO templatelinks (tl_from, tl_namespace, tl_title)"
1665             . " VALUES ($page_id, ?, ?)";
1666 0 0         $st1 = $dbh->prepare ($sql) or croak $dbh->errstr;
1667 0           foreach my $link (sort @templatelinks)
1668             {
1669 0           my $en = ($link)[0];
1670 0           my ($ns, $t) = $self->__namespace_to_num ($en);
1671              
1672 0           $st1->execute ($ns, $t);
1673             }
1674 0           $st1->finish;
1675            
1676             # Insert into the externallinks table.
1677 0           $sql = "INSERT INTO externallinks (el_from, el_to, el_index)"
1678             . " VALUES ($page_id, ?, '')";
1679 0 0         $st1 = $dbh->prepare ($sql) or croak $dbh->errstr;
1680 0           foreach my $link (sort @externallinks)
1681             {
1682 0           my $en = ($link)[0];
1683 0           $st1->execute ($dbh->quote($en));
1684             }
1685 0           $st1->finish;
1686             }
1687             }
1688              
1689             =head2 write_node_post_locking
1690              
1691             Like the parent function, but works with the mediawiki DB.
1692              
1693             =cut
1694              
1695             sub write_node_post_locking
1696             {
1697 0     0 1   my ($self, %args) = @_;
1698 0           my ($node, $content,
1699             $links_to_ref, $metadata, $requires_moderation) = @args{qw(node content links_to
1700             metadata requires_moderation)};
1701 0           my $dbh = $self->dbh;
1702              
1703 0 0 0       croak "write_node_post_locking requires edit_type, and remote_ip metadata"
1704             unless $metadata && $metadata->{edit_type};
1705              
1706 0           my $timestamp = $self->_get_timestamp ();
1707 0 0         my @links_to = @{$links_to_ref || []}; # default to empty array
  0            
1708              
1709 0           my ($ns, $name) = $self->__namespace_to_num ($node);
1710 0           my $sql;
1711              
1712             my $userid;
1713 0           my $username;
1714 0 0         if ($metadata->{username})
1715             {
1716 0           $sql = "SELECT user_id, user_name FROM user"
1717             . " WHERE "
1718             . $self->_get_cmp_sql ("user_name",
1719             $metadata->{username},
1720             $args{ignore_case});
1721 0 0         my $rec = _utf8_on_array $dbh->selectrow_arrayref ($sql)
1722             or croak "unable to retrieve user `$username': " . $dbh->errstr;
1723 0           $userid = $rec->[0];
1724 0           $username = $rec->[1];
1725             }
1726             else
1727             {
1728 0           $username = $metadata->{remote_ip};
1729 0           $userid = 0;
1730             }
1731              
1732             # First, remember the previous version number.
1733 0           my $old_old_id = $self->get_current_version ($node);
1734              
1735             # Always insert into text table.
1736 0           $sql = "INSERT INTO "
1737             . "text (old_text, old_flags)"
1738             . " VALUES (". $dbh->quote ($content)
1739             . ", 'utf-8')";
1740              
1741 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1742 0 0         my $new_old_id = $dbh->last_insert_id (undef, undef, undef, undef)
1743             or croak "Error retrieving last insert id: " . $dbh->errstr;
1744              
1745             # Either inserting a new page or updating an old one.
1746 0           my $page_id;
1747 0 0         if ($old_old_id)
1748             {
1749 0           $sql = "SELECT page_id FROM page"
1750             . " WHERE page_namespace = $ns"
1751             . " AND "
1752             . $self->_get_cmp_sql ("page_title",
1753             $name,
1754             $args{ignore_case});
1755 0 0         $page_id = _utf8_on_array $dbh->selectrow_arrayref ($sql)->[0]
1756             or croak "Error retrieving page id: " . $dbh->errstr;
1757              
1758 0           $sql = "UPDATE page SET page_touched = " . $dbh->quote ($timestamp)
1759             . ", "
1760             . "page_is_redirect = 0, "
1761             . "page_is_new = 0, "
1762             . "page_latest = $new_old_id, "
1763             . "page_len = "
1764             . length ($content)
1765             . " WHERE page_id = $page_id";
1766 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1767             }
1768             else{
1769 0 0         $page_id = $dbh->last_insert_id (undef, undef, undef, undef)
1770             or croak "Error retrieving last insert id: " . $dbh->errstr;
1771              
1772 0           $sql = "INSERT INTO page (page_namespace, page_title, page_touched, "
1773             . "page_counter, page_is_redirect, "
1774             . "page_is_new, page_random, page_latest, "
1775             . "page_len, page_restrictions)"
1776             . " VALUES ($ns, "
1777             . $dbh->quote ($name) . ", "
1778             . $dbh->quote ($timestamp)
1779             . ", 0, 0, 1, 0, $new_old_id, "
1780             . length ($content) . ", 'autoconfirmed')";
1781 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1782              
1783 0 0         $page_id = $dbh->last_insert_id (undef, undef, undef, undef)
1784             or croak "Error retrieving last insert id: " . $dbh->errstr;
1785             }
1786              
1787             # Always insert into the recent changes table.
1788 0 0         $sql = "INSERT INTO "
    0          
    0          
1789             . "recentchanges (rc_timestamp, rc_cur_time, rc_user, "
1790             . "rc_user_text, rc_namespace, rc_title, "
1791             . "rc_comment, rc_minor, rc_bot, rc_new, "
1792             . "rc_cur_id, rc_this_oldid, rc_last_oldid, "
1793             . "rc_type, rc_moved_to_ns, rc_patrolled, rc_ip)"
1794             . " VALUES ("
1795             . $dbh->quote ($timestamp) . ", "
1796             . $dbh->quote ($timestamp)
1797             . ", $userid, "
1798             . $dbh->quote ($username)
1799             . ", $ns, "
1800             . $dbh->quote ($name) . ", "
1801             . $dbh->quote ($metadata->{comment}) . ", "
1802             . ($metadata->{edit_type} eq 'Minor tidying' ? 1 : 0)
1803             . ", 0, "
1804             . (defined $old_old_id ? 0 : 1)
1805             . ", $page_id, $new_old_id, "
1806             . (defined $old_old_id ? $old_old_id : 0)
1807             . ", 0, $ns, 0,"
1808             . $dbh->quote ($metadata->{remote_ip})
1809             . ")";
1810 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1811              
1812 0 0 0       $self->moderate_node (version => $new_old_id)
1813             if ($metadata->{auto_patrolled} eq 'yes' || !$requires_moderation);
1814              
1815             # Always insert into revision
1816 0 0         $sql = "INSERT INTO "
1817             . "revision (rev_timestamp, rev_user, "
1818             . "rev_user_text, "
1819             . "rev_comment, rev_minor_edit, rev_page, "
1820             . "rev_text_id)"
1821             . " VALUES ("
1822             . $dbh->quote ($timestamp)
1823             . ", $userid, "
1824             . $dbh->quote ($username).", "
1825             . $dbh->quote ($metadata->{comment}) . ", "
1826             . ($metadata->{edit_type} eq 'Minor tidying' ? 1 : 0)
1827             . ", $page_id, $new_old_id "
1828             . ")";
1829 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
1830              
1831 0           $self->update_links (name => $node, links => \@links_to);
1832              
1833             # And also store any metadata. Note that any entries already in the
1834             # metadata table refer to old versions, so we don't need to delete them.
1835 0           foreach my $type (keys %$metadata)
1836             {
1837 0 0         croak "unknown metadata key `$type'"
1838             unless grep qr/^\Q$type\E$/, (qw{comment edit_type formatter
1839             username remote_ip});
1840             }
1841              
1842             # Finally call post_write on any plugins.
1843 0 0         my @plugins = @{$args{plugins} || [ ]};
  0            
1844 0           foreach my $plugin (@plugins) {
1845 0 0         if ($plugin->can ("post_write"))
1846             {
1847 0           $plugin->post_write (node => $node,
1848             version => $new_old_id,
1849             content => $content,
1850             metadata => $metadata);
1851             }
1852             }
1853              
1854 0           return 1;
1855             }
1856              
1857              
1858              
1859             =head2 node_exists
1860              
1861             $store->node_exists ($node);
1862             $store->node_exists (name => $node, %other_args);
1863              
1864             Like the parent function of the same name, but much faster. Really just
1865             a wrapper for get_current_version, returns the current version number when
1866             it exists and undef otherwise.
1867              
1868             =cut
1869              
1870             sub node_exists
1871             {
1872 0     0 1   my $self = shift;
1873 0           return $self->get_current_version (@_);
1874             }
1875              
1876              
1877              
1878             =head2 list_backlinks
1879              
1880             # List all nodes that link to the Home Page.
1881             my @links = $store->list_backlinks (node => "Home Page");
1882              
1883             =cut
1884              
1885             sub list_backlinks
1886             {
1887 0     0 1   my ($self, %args) = @_;
1888 0           my $node = $args{node};
1889 0 0         croak "Must supply a node name" unless $node;
1890              
1891 0           my ($ns, $name) = $self->__namespace_to_num ($node);
1892 0           my $dbh = $self->dbh;
1893              
1894 0           my $fields = "DISTINCT page_namespace, page_title";
1895 0 0         $fields = "COUNT($fields)" unless wantarray;
1896              
1897 0           my $sql = "SELECT $fields"
1898             . " FROM page p, pagelinks pl"
1899             . " WHERE pl_namespace = $ns"
1900             . " AND "
1901             . $self->_get_cmp_sql ("pl_title",
1902             $name,
1903             $args{ignore_case})
1904             . " AND page_id = pl_from";
1905              
1906 0           my $limoffsql = _get_lim_off_sql (%args);
1907 0 0         $sql .= " " . $limoffsql if $limoffsql;
1908              
1909 0           my $sth = $dbh->prepare ($sql);
1910 0 0         $sth->execute or croak $dbh->errstr;
1911              
1912 0 0         return ($sth->fetchrow_array)[0] unless wantarray;
1913              
1914 0           my @backlinks;
1915 0           while (my ($ns_from, $from) = _utf8_on_array $sth->fetchrow_array)
1916             {
1917 0           push @backlinks, $self->__num_to_namespace ($ns_from, $from);
1918             }
1919 0           return @backlinks;
1920             }
1921              
1922              
1923              
1924             =head2 list_dangling_links
1925              
1926             # List all nodes that have been linked to from other nodes but don't
1927             # yet exist.
1928             my @links = $store->list_dangling_links;
1929              
1930             Each node is returned once only, regardless of how many other nodes
1931             link to it. Nodes are be returned unsorted.
1932              
1933             =cut
1934              
1935             sub list_dangling_links
1936             {
1937 0     0 1   my $self = shift;
1938 0           my $dbh = $self->dbh;
1939 0           my $sql = "SELECT DISTINCT *"
1940             . " FROM pagelinks LEFT JOIN page ON pl_title=page_title AND pl_namespace=page_namespace"
1941             . " WHERE page_id IS NULL";
1942 0           my $sth = $dbh->prepare ($sql);
1943 0 0         $sth->execute or croak $dbh->errstr;
1944 0           my @links;
1945 0           while (my ($link) = _utf8_on_array $sth->fetchrow_array)
1946             {
1947 0           push @links, $link;
1948             }
1949 0           return @links;
1950             }
1951              
1952              
1953              
1954             =head2 list_dangling_links_w_count
1955              
1956             # List all nodes that have been linked to from other nodes but don't
1957             # yet exist, with a reference count.
1958             foreach my $link ($store->list_dangling_links_w_count)
1959             {
1960             print "Missing `", $link->[0], "' has ", $link->[1], " references.\n";
1961             }
1962              
1963             Nodes are returned sorted primarily by the reference count, greatest first, and
1964             secondarily in alphabetical order.
1965              
1966             =cut
1967              
1968             sub list_dangling_links_w_count
1969             {
1970 0     0 1   my ($self, %args) = @_;
1971 0           my $dbh = $self->dbh;
1972 0           my ($fields, $tail);
1973              
1974 0 0         if (wantarray)
1975             {
1976 0           $fields = "pl_namespace,pl_title, COUNT(*)";
1977 0           $tail = "GROUP BY pl_namespace, pl_title ORDER BY COUNT(*) DESC, pl_namespace, pl_title";
1978             }
1979             else
1980             {
1981 0           $fields = "COUNT(DISTINCT pl_namespace, pl_title)";
1982             }
1983              
1984 0           my $limoffsql = _get_lim_off_sql (%args);
1985 0 0         $tail .= ($tail ? " " : "") . $limoffsql if $limoffsql;
    0          
1986              
1987 0           my $sql = "SELECT $fields FROM"
1988             . " pagelinks LEFT JOIN page ON pl_title=page_title AND pl_namespace=page_namespace"
1989             . " WHERE page_id IS NULL";
1990 0 0         $sql .= " " . $tail if $tail;
1991              
1992 0           my $sth = $dbh->prepare ($sql);
1993 0 0         $sth->execute or croak $dbh->errstr;
1994              
1995 0 0         return ($sth->fetchrow_array)[0] unless wantarray;
1996              
1997 0           my @links;
1998 0           while (my @row = _utf8_on_array $sth->fetchrow_array)
1999             {
2000 0           push @links, [($self->__num_to_namespace ($row[0], $row[1])), $row[2]];
2001             }
2002 0           return @links;
2003             }
2004              
2005              
2006             =head2 get_user_groups
2007             $config{store}->get_user_groups(name => $user_name);
2008             or
2009             $config{store}->get_user_groups(id => $user_id);
2010             or
2011             $config{store}->get_user_groups();
2012            
2013             Given a valid user name, or user id, this function
2014             will return an array of the group names for the groups the user belongs to.
2015              
2016             Given no arguments it will return an array of available group names.
2017              
2018             =cut
2019             sub get_user_groups
2020             {
2021 0     0 1   my ($self, %args) = @_;
2022 0           my $dbh = $self->{_dbh};
2023              
2024 0           my $sql = "SELECT DISTINCT(ug_group) from user_groups";#no args received will default here
2025              
2026 0 0         $sql = "SELECT ug_group"
2027             . " FROM user JOIN user_groups ON user_id = ug_user"
2028             . " WHERE "
2029             . $self->_get_cmp_sql ("user_name",
2030             $args{name},
2031             $args{ignore_case})
2032             if $args{name};
2033              
2034 0 0         $sql = "SELECT ug_group FROM user_groups WHERE user_id = " . $dbh->quote($args{id})
2035             if $args{id};
2036              
2037 0 0         my $usergroups = _utf8_on_array $dbh->selectall_arrayref ($sql)
2038             or croak "Error retrieving user info: " . $dbh->errstr;
2039              
2040 0           my @groups = map {$_->[0]} @$usergroups;
  0            
2041              
2042 0           return @groups;
2043             }
2044              
2045              
2046              
2047             =head2 get_user_info
2048              
2049             my ($username, $email_validated, $token)
2050             = $store->get_user_info (name => $username,
2051             password => $password,
2052             fields => [name, email_authenticated,
2053             token],
2054             %other_args);
2055              
2056             Given a user name, return the requested fields if the user exists and undef,
2057             otherwise. Given a password or a token, undef is also returned if the
2058             specified password or token is incorrect.
2059              
2060             The list of fields to return defaults to C, C,
2061             & C.
2062              
2063             The returned user name may be different from the one passed in when
2064             $args{ignore_case} is set.
2065              
2066             When an email_token is supplied and validated, the user's email is
2067             automatically marked as authenticated in the database.
2068              
2069             =cut
2070              
2071             sub get_user_info
2072             {
2073 0     0 1   my ($self, %args) = @_;
2074 0           my $dbh = $self->{_dbh};
2075              
2076 0           my ($where, $count);
2077 0           for my $key (qw{name id email})
2078             {
2079 0 0         if ($args{$key})
2080             {
2081 0           $count++;
2082 0 0         $where = $args{id}
    0          
2083             ? "user_id = " . $args{id}
2084             : $self->_get_cmp_sql ("user_$key",
2085             $args{$key},
2086             $args{email}
2087             ? 1 : $args{ignore_case});
2088             }
2089             }
2090 0 0         croak "Must supply one and only one of `name', `id', or `email'"
2091             unless $count == 1;
2092              
2093 0           $count = 0;
2094 0           for my $key (qw{password token email_token})
2095             {
2096 0 0         if (exists $args{$key}) {
2097 0           $count++;
2098 0 0         croak "Undefined value supplied for `$key'"
2099             unless defined $args{$key};
2100             }
2101             }
2102 0 0         croak "Must supply only one of `password', `token', or `email_token'"
2103             if $count > 1;
2104              
2105 0           my @fields = map {"user_$_"}
  0            
2106 0 0         ($args{fields} ? @{$args{fields}}
2107             : qw(name email_authenticated token));
2108              
2109 0 0         if (defined $args{password})
    0          
    0          
2110             {
2111 0           push @fields, qw(user_id user_password);
2112             }
2113             elsif (defined $args{token})
2114             {
2115 0           push @fields, qw(user_token);
2116             }
2117             elsif (defined $args{email_token})
2118             {
2119 0           push @fields, qw(user_id user_email_token user_email_token_expires);
2120             }
2121              
2122 0           my $sql = "SELECT " . join (", ", @fields)
2123             . " FROM user"
2124             . " WHERE $where";
2125              
2126 0 0         my $userinfo = _utf8_on_array $dbh->selectall_arrayref ($sql)
2127             or croak "Error retrieving user info: " . $dbh->errstr;
2128              
2129             # Check that one and only one user was found.
2130 0 0         return undef unless @$userinfo; # failed login
2131 0 0         die "multiple users found matching `$args{name}'"
2132             unless @$userinfo == 1; # Corrupt database.
2133              
2134 0           $userinfo = $userinfo->[0];
2135              
2136 0 0         if (defined $args{password})
    0          
    0          
2137             {
2138             # Check the password.
2139 0           my ($uid, $password);
2140 0           $password = pop @$userinfo;
2141 0           $uid = pop @$userinfo;
2142              
2143 0           my $ep = md5_hex ($uid . "-" . md5_hex ($args{password}));
2144 0 0         return undef unless $ep eq $password;
2145             }
2146             elsif (defined $args{token})
2147             {
2148             # Check the token.
2149 0           my $token = pop @$userinfo;
2150 0 0         return undef unless $args{token} eq $token;
2151             }
2152             elsif (defined $args{email_token})
2153             {
2154             # Check the token.
2155 0           my ($uid, $expires, $token);
2156 0           $expires = $self->_make_date (pop @$userinfo);
2157 0           $token = pop @$userinfo;
2158 0           $uid = pop @$userinfo;
2159 0           my $now = gmtime;
2160              
2161             return undef
2162 0 0 0       unless $args{email_token} eq $token
2163             && $now < $expires;
2164              
2165 0           $self->update_user (id => $uid, email_authenticated => $now);
2166             }
2167              
2168             # The remaining fields were requested.
2169 0           for (my $i = 0; $i < @fields; $i++)
2170             {
2171 0 0 0       $userinfo->[$i] = $self->_make_date ($userinfo->[$i])
2172             if defined $userinfo->[$i] && $fields[$i] =~ /_(?:touched|expires)$/;
2173             }
2174 0           return @$userinfo;
2175             }
2176              
2177              
2178              
2179             =head2 add_to_block_list
2180              
2181             my @errmsgs = $store->add_to_block_list (blockee => $b, expiry => $e,
2182             reason => $r);
2183              
2184             Add new user or ip/netmask to the ipblocks table.
2185              
2186             C can be either a username that must exist in the user table, or an ip
2187             address with an optional ip mask. C the date for when the block
2188             expires. This should be either seconds since the epoch or a
2189             L. C will be the moderators reason for the
2190             blocking.
2191              
2192             =cut
2193              
2194             sub add_to_block_list
2195 0     0 1   {
2196            
2197             }
2198              
2199              
2200             =head2 create_new_user
2201              
2202             my @errmsgs = $store->create_new_user (name => $username, password => $p);
2203              
2204             Create a new user. C and C are required arguments.
2205             Optional arguments are C & C.
2206              
2207             Returns a potentially empty list of error messages.
2208              
2209             =cut
2210              
2211             # Internal function to create and update users.
2212             #
2213             # This function makes some assumptions enforced by its callers. Don't use it
2214             # directly.
2215             sub _update_user
2216             {
2217 0     0     my ($self, %args) = @_;
2218              
2219 0           my $dbh = $self->{_dbh};
2220              
2221             # Fields to update/insert.
2222 0           my (@fields, @values);
2223              
2224             # For the timestamp, and perhaps email_token_expires.
2225 0           my $now = gmtime;
2226 0           $args{touched} = $now;
2227              
2228 0 0 0       $args{email_token_expires} = $now + $args{email_token_expires}
      0        
2229             if exists $args{email_token_expires}
2230             && !(ref $args{email_token_expires}
2231             && $args{email_token_expires}->isa ('Time::Piece'));
2232              
2233 0           my @infields = qw(real_name email email_token email_token_expires
2234             email_authenticated token touched);
2235 0 0         push @infields, "name" if $args{create};
2236 0           for my $field (@infields)
2237             {
2238 0 0         if (exists $args{$field})
2239             {
2240 0           push @fields, "user_$field";
2241 0 0         if (defined $args{$field})
2242             {
2243 0 0 0       $args{$field}->set_stringify ($timestamp_fmt)
2244             if ref $args{$field}
2245             && $args{$field}->isa ('Time::Piece::Adaptive');
2246 0           push @values,
2247             $dbh->quote ($args{$field});
2248             }
2249             else
2250             {
2251 0           push @values, "NULL";
2252             }
2253             }
2254             }
2255              
2256             # touched and name are always included.
2257 0 0 0       croak "Must include at least one field for update"
    0          
2258             unless $args{password} || @fields > ($args{create} ? 2 : 1);
2259              
2260 0           my $uid;
2261             my $sql;
2262 0 0         if ($args{create})
2263             {
2264 0           $sql = "INSERT INTO user (" . join (", ", @fields)
2265             . ") VALUES (" . join (", ", @values) . ")";
2266             }
2267             else
2268             {
2269 0           my %qa;
2270 0 0         if ($args{id})
2271             {
2272 0           $qa{id} = $args{id};
2273             }
2274             else
2275             {
2276 0           $qa{name} = $args{name};
2277             }
2278 0           ($uid) = $self->get_user_info (%qa, fields => ["id"]);
2279              
2280             # Check that one and only one existing user was found.
2281 0 0         return "No such user, `" . $args{name} . "'."
2282             unless $uid;
2283              
2284 0           $sql = "UPDATE user SET "
2285 0           . join (", ", map ({"$fields[$_] = $values[$_]"} (0..$#fields)))
2286             . " WHERE "
2287             . "user_id = " . $uid;
2288             }
2289              
2290 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
2291              
2292 0 0         if ($args{create})
2293             {
2294             # Get the new user ID and update the password.
2295 0 0         $uid = $dbh->last_insert_id (undef, undef, undef, undef)
2296             or croak "Error retrieving last insert id: " . $dbh->errstr;
2297             }
2298              
2299 0 0         if ($args{password})
2300             {
2301             # Encode the password.
2302 0           my $ep = md5_hex ($uid . "-" . md5_hex ($args{password}));
2303              
2304             # Update the password.
2305 0           $sql = "UPDATE user SET user_password = " . $dbh->quote ($ep)
2306             . " WHERE user_id = $uid";
2307 0 0         $dbh->do ($sql) or croak "Error updating database: " . $dbh->errstr;
2308             }
2309              
2310 0           return;
2311             }
2312              
2313             sub create_new_user
2314             {
2315 0     0 1   my ($self, %args) = @_;
2316              
2317 0 0         croak "name is a required argument" unless $args{name};
2318 0 0         croak "password is a required argument" unless $args{password};
2319              
2320 0           my $dbh = $self->{_dbh};
2321              
2322             # Verify that the user does not exist.
2323 0           my $sql = "SELECT user_name FROM user"
2324             . " WHERE "
2325             . $self->_get_cmp_sql ("user_name",
2326             $args{name},
2327             $args{ignore_case});
2328 0 0         my $userinfo = _utf8_on_array $dbh->selectall_arrayref ($sql)
2329             or croak "Error retrieving user info: " . $dbh->errstr;
2330              
2331             # Check no existing user was found.
2332 0 0         return "User `" . $userinfo->[0]->[0] . "' already exists."
2333             if @$userinfo;
2334              
2335 0           return $self->_update_user (%args, create => 1);
2336             }
2337              
2338              
2339              
2340             =head2 update_user
2341              
2342             Like C, except only either C or C, and one field to
2343             update, are required arguments.
2344              
2345             =cut
2346              
2347             sub update_user
2348             {
2349 0     0 1   my ($self, %args) = @_;
2350              
2351 0 0 0       croak "One, and only one, of `name' and `id', are required arguments."
      0        
      0        
2352             unless !($args{name} && $args{id}) && ($args{name} || $args{id});
2353              
2354 0           return $self->_update_user (%args);
2355             }
2356              
2357              
2358              
2359             =head2 schema_current
2360              
2361             Overrides the parent function of the same name. At the moment it only returns
2362             (0, 0).
2363              
2364             =cut
2365              
2366             sub schema_current
2367             {
2368 0     0 1   return (0, 0);
2369             }
2370              
2371             =head2 get_interwiki_url
2372              
2373             $url = $store->get_interwiki_url ($wikilink);
2374              
2375             Converts an interwiki link (like C) to a URL (in this example,
2376             something like C), or returns undef if
2377             C<$wikilink> does not appear to refer to a known wiki. This match is always
2378             case insensitive because users are often careless.
2379              
2380             =cut
2381              
2382             # Hrm. It seems silly to make these errors fatal. Perhaps it should be a
2383             # configuration option.
2384             sub get_interwiki_url
2385             {
2386 0     0 1   my ($self, $wl) = @_;
2387 0           my $dbh = $self->{_dbh};
2388              
2389 0           my ($prefix, $suffix) = ($wl =~ /^([^:]*):+([^:].*)$/);
2390 0 0         return unless $prefix;
2391              
2392 0           my $sql = "SELECT iw_url FROM interwiki"
2393             . " WHERE "
2394             . $self->_get_cmp_sql ("iw_prefix",
2395             $prefix, 1);
2396 0 0         my $rows = _utf8_on_array $dbh->selectall_arrayref ($sql)
2397             or croak "Error retrieving interwiki info: " . $dbh->errstr;
2398              
2399 0 0         warn "Multiple interwiki entries found for `$prefix'."
2400             if @$rows > 1;
2401 0 0         return unless @$rows == 1;
2402              
2403 0           my $url = $rows->[0][0];
2404 0           $url =~ s/\$1/$suffix/;
2405 0           return $url;
2406             }
2407              
2408              
2409              
2410             =head1 SEE ALSO
2411              
2412             =over 4
2413              
2414             =item L
2415              
2416             =item L
2417              
2418             =item L
2419              
2420             =item L
2421              
2422             =item L
2423              
2424             =item L
2425              
2426             =back
2427              
2428             =head1 AUTHOR
2429              
2430             Derek Price, C<< >>
2431              
2432             =head1 BUGS
2433              
2434             Please report any bugs or feature requests to
2435             C, or through the web interface at
2436             L.
2437             I will be notified, and then you'll automatically be notified of progress on
2438             your bug as I make changes.
2439              
2440             =head1 SUPPORT
2441              
2442             You can find documentation for this module with the perldoc command.
2443              
2444             perldoc Wiki::Toolkit::Store::Mediawiki
2445              
2446             You can also look for information at:
2447              
2448             =over 4
2449              
2450             =item * AnnoCPAN: Annotated CPAN documentation
2451              
2452             L
2453              
2454             =item * CPAN Ratings
2455              
2456             L
2457              
2458             =item * RT: CPAN's request tracker
2459              
2460             L
2461              
2462             =item * Search CPAN
2463              
2464             L
2465              
2466             =back
2467              
2468             =head1 ACKNOWLEDGEMENTS
2469              
2470             My thanks go to Kake Pugh, for providing the well written L and
2471             L modules, which got me started on this.
2472              
2473             =head1 COPYRIGHT & LICENSE
2474              
2475             Copyright 2006 Derek Price, all rights reserved.
2476              
2477             This program is free software; you can redistribute it and/or modify it
2478             under the same terms as Perl itself.
2479              
2480             =cut
2481              
2482             1; # End of Wiki::Toolkit::Store::Mediawiki
2483             # vim:tabstop=8:shiftwidth=4