| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | package Wiki::Toolkit::Store::Database; | 
| 2 |  |  |  |  |  |  |  | 
| 3 | 8 |  |  | 8 |  | 3563 | use strict; | 
|  | 8 |  |  |  |  | 33 |  | 
|  | 8 |  |  |  |  | 297 |  | 
| 4 |  |  |  |  |  |  |  | 
| 5 | 8 |  |  | 8 |  | 44 | use vars qw( $VERSION $timestamp_fmt ); | 
|  | 8 |  |  |  |  | 15 |  | 
|  | 8 |  |  |  |  | 686 |  | 
| 6 |  |  |  |  |  |  | $timestamp_fmt = "%Y-%m-%d %H:%M:%S"; | 
| 7 |  |  |  |  |  |  |  | 
| 8 | 8 |  |  | 8 |  | 8018 | use DBI; | 
|  | 8 |  |  |  |  | 96842 |  | 
|  | 8 |  |  |  |  | 450 |  | 
| 9 | 8 |  |  | 8 |  | 5136 | use Time::Piece; | 
|  | 8 |  |  |  |  | 100188 |  | 
|  | 8 |  |  |  |  | 39 |  | 
| 10 | 8 |  |  | 8 |  | 659 | use Time::Seconds; | 
|  | 8 |  |  |  |  | 20 |  | 
|  | 8 |  |  |  |  | 581 |  | 
| 11 | 8 |  |  | 8 |  | 47 | use Carp qw( carp croak ); | 
|  | 8 |  |  |  |  | 32 |  | 
|  | 8 |  |  |  |  | 452 |  | 
| 12 | 8 |  |  | 8 |  | 48 | use Digest::MD5 qw( md5_hex ); | 
|  | 8 |  |  |  |  | 15 |  | 
|  | 8 |  |  |  |  | 630 |  | 
| 13 |  |  |  |  |  |  |  | 
| 14 |  |  |  |  |  |  | $VERSION = '0.32'; | 
| 15 |  |  |  |  |  |  | my $SCHEMA_VER = 11; | 
| 16 |  |  |  |  |  |  |  | 
| 17 |  |  |  |  |  |  | # first, detect if Encode is available - it's not under 5.6. If we _are_ | 
| 18 |  |  |  |  |  |  | # under 5.6, give up - we'll just have to hope that nothing explodes. This | 
| 19 |  |  |  |  |  |  | # is the current 0.54 behaviour, so that's ok. | 
| 20 |  |  |  |  |  |  |  | 
| 21 |  |  |  |  |  |  | my $CAN_USE_ENCODE; | 
| 22 |  |  |  |  |  |  | BEGIN { | 
| 23 | 8 |  |  | 8 |  | 676 | eval " use Encode "; | 
|  | 8 |  |  | 8 |  | 2493 |  | 
|  | 8 |  |  |  |  | 42972 |  | 
|  | 8 |  |  |  |  | 603 |  | 
| 24 | 8 | 50 |  |  |  | 71534 | $CAN_USE_ENCODE = $@ ? 0 : 1; | 
| 25 |  |  |  |  |  |  | } | 
| 26 |  |  |  |  |  |  |  | 
| 27 |  |  |  |  |  |  | =head1 NAME | 
| 28 |  |  |  |  |  |  |  | 
| 29 |  |  |  |  |  |  | Wiki::Toolkit::Store::Database - parent class for database storage backends | 
| 30 |  |  |  |  |  |  | for Wiki::Toolkit | 
| 31 |  |  |  |  |  |  |  | 
| 32 |  |  |  |  |  |  | =head1 SYNOPSIS | 
| 33 |  |  |  |  |  |  |  | 
| 34 |  |  |  |  |  |  | This is probably only useful for Wiki::Toolkit developers. | 
| 35 |  |  |  |  |  |  |  | 
| 36 |  |  |  |  |  |  | # See below for parameter details. | 
| 37 |  |  |  |  |  |  | my $store = Wiki::Toolkit::Store::MySQL->new( %config ); | 
| 38 |  |  |  |  |  |  |  | 
| 39 |  |  |  |  |  |  | =head1 METHODS | 
| 40 |  |  |  |  |  |  |  | 
| 41 |  |  |  |  |  |  | =over 4 | 
| 42 |  |  |  |  |  |  |  | 
| 43 |  |  |  |  |  |  | =item B | 
| 44 |  |  |  |  |  |  |  | 
| 45 |  |  |  |  |  |  | my $store = Wiki::Toolkit::Store::MySQL->new( dbname  => "wiki", | 
| 46 |  |  |  |  |  |  | dbuser  => "wiki", | 
| 47 |  |  |  |  |  |  | dbpass  => "wiki", | 
| 48 |  |  |  |  |  |  | dbhost  => "db.example.com", | 
| 49 |  |  |  |  |  |  | dbport  => 1234, | 
| 50 |  |  |  |  |  |  | charset => "iso-8859-1" ); | 
| 51 |  |  |  |  |  |  | or | 
| 52 |  |  |  |  |  |  |  | 
| 53 |  |  |  |  |  |  | my $store = Wiki::Toolkit::Store::MySQL->new( dbh => $dbh ); | 
| 54 |  |  |  |  |  |  |  | 
| 55 |  |  |  |  |  |  | C is optional, defaults to C, and does nothing | 
| 56 |  |  |  |  |  |  | unless you're using perl 5.8 or newer. | 
| 57 |  |  |  |  |  |  |  | 
| 58 |  |  |  |  |  |  | If you do not provide an active database handle in C, then | 
| 59 |  |  |  |  |  |  | C is mandatory. C, C, C and C | 
| 60 |  |  |  |  |  |  | are optional, but you'll want to supply them unless your database's | 
| 61 |  |  |  |  |  |  | connection method doesn't require them. | 
| 62 |  |  |  |  |  |  |  | 
| 63 |  |  |  |  |  |  | If you do provide C then it must have the following | 
| 64 |  |  |  |  |  |  | parameters set; otherwise you should just provide the connection | 
| 65 |  |  |  |  |  |  | information and let us create our own handle: | 
| 66 |  |  |  |  |  |  |  | 
| 67 |  |  |  |  |  |  | =over 4 | 
| 68 |  |  |  |  |  |  |  | 
| 69 |  |  |  |  |  |  | =item * | 
| 70 |  |  |  |  |  |  |  | 
| 71 |  |  |  |  |  |  | C = 1 | 
| 72 |  |  |  |  |  |  |  | 
| 73 |  |  |  |  |  |  | =item * | 
| 74 |  |  |  |  |  |  |  | 
| 75 |  |  |  |  |  |  | C = 0 | 
| 76 |  |  |  |  |  |  |  | 
| 77 |  |  |  |  |  |  | =item * | 
| 78 |  |  |  |  |  |  |  | 
| 79 |  |  |  |  |  |  | C = 1 | 
| 80 |  |  |  |  |  |  |  | 
| 81 |  |  |  |  |  |  | =back | 
| 82 |  |  |  |  |  |  |  | 
| 83 |  |  |  |  |  |  | =cut | 
| 84 |  |  |  |  |  |  |  | 
| 85 |  |  |  |  |  |  | sub new { | 
| 86 | 2 |  |  | 2 | 1 | 183 | my ($class, @args) = @_; | 
| 87 | 2 |  |  |  |  | 6 | my $self = {}; | 
| 88 | 2 |  |  |  |  | 6 | bless $self, $class; | 
| 89 | 2 |  |  |  |  | 13 | return $self->_init(@args); | 
| 90 |  |  |  |  |  |  | } | 
| 91 |  |  |  |  |  |  |  | 
| 92 |  |  |  |  |  |  | sub _init { | 
| 93 | 3 |  |  | 3 |  | 12 | my ($self, %args) = @_; | 
| 94 |  |  |  |  |  |  |  | 
| 95 | 3 | 50 |  |  |  | 16 | if ( $args{dbh} ) { | 
| 96 | 0 |  |  |  |  | 0 | $self->{_dbh} = $args{dbh}; | 
| 97 | 0 |  |  |  |  | 0 | $self->{_external_dbh} = 1; # don't disconnect at DESTROY time | 
| 98 | 0 |  | 0 |  |  | 0 | $self->{_charset} = $args{charset} || "iso-8859-1"; | 
| 99 |  |  |  |  |  |  | } else { | 
| 100 | 3 | 50 |  |  |  | 43 | die "Must supply a dbname" unless defined $args{dbname}; | 
| 101 | 0 |  |  |  |  | 0 | $self->{_dbname} = $args{dbname}; | 
| 102 | 0 |  | 0 |  |  | 0 | $self->{_dbuser} = $args{dbuser} || ""; | 
| 103 | 0 |  | 0 |  |  | 0 | $self->{_dbpass} = $args{dbpass} || ""; | 
| 104 | 0 |  | 0 |  |  | 0 | $self->{_dbhost} = $args{dbhost} || ""; | 
| 105 | 0 |  | 0 |  |  | 0 | $self->{_dbport} = $args{dbport} || ""; | 
| 106 | 0 |  | 0 |  |  | 0 | $self->{_charset} = $args{charset} || "iso-8859-1"; | 
| 107 |  |  |  |  |  |  |  | 
| 108 |  |  |  |  |  |  | # Connect to database and store the database handle. | 
| 109 |  |  |  |  |  |  | my ($dbname, $dbuser, $dbpass, $dbhost, $dbport) = | 
| 110 | 0 |  |  |  |  | 0 | @$self{qw(_dbname _dbuser _dbpass _dbhost _dbport)}; | 
| 111 | 0 | 0 |  |  |  | 0 | my $dsn = $self->_dsn($dbname, $dbhost, $dbport) | 
| 112 |  |  |  |  |  |  | or croak "No data source string provided by class"; | 
| 113 | 0 | 0 |  |  |  | 0 | $self->{_dbh} = DBI->connect( $dsn, $dbuser, $dbpass, | 
| 114 |  |  |  |  |  |  | $self->_get_dbh_connect_attr ) | 
| 115 |  |  |  |  |  |  | or croak "Can't connect to database $dbname using $dsn: " | 
| 116 |  |  |  |  |  |  | . DBI->errstr; | 
| 117 |  |  |  |  |  |  | } | 
| 118 |  |  |  |  |  |  |  | 
| 119 | 0 |  |  |  |  | 0 | my ($cur_ver, $db_ver) = $self->schema_current; | 
| 120 | 0 | 0 |  |  |  | 0 | if ($db_ver < $cur_ver) { | 
|  |  | 0 |  |  |  |  |  | 
| 121 | 0 |  |  |  |  | 0 | croak "Database schema version $db_ver is too old (need $cur_ver)"; | 
| 122 |  |  |  |  |  |  | } elsif ($db_ver > $cur_ver) { | 
| 123 | 0 |  |  |  |  | 0 | croak "Database schema version $db_ver is too new (need $cur_ver)"; | 
| 124 |  |  |  |  |  |  | } | 
| 125 |  |  |  |  |  |  |  | 
| 126 | 0 |  |  |  |  | 0 | return $self; | 
| 127 |  |  |  |  |  |  | } | 
| 128 |  |  |  |  |  |  |  | 
| 129 |  |  |  |  |  |  | # Internal method to get attributes for passing to DBI->connect(). | 
| 130 |  |  |  |  |  |  | # Override in subclasses to add database-dependent attributes. | 
| 131 |  |  |  |  |  |  | sub _get_dbh_connect_attr { | 
| 132 |  |  |  |  |  |  | return { | 
| 133 | 0 |  |  | 0 |  | 0 | PrintError => 0, | 
| 134 |  |  |  |  |  |  | RaiseError => 1, | 
| 135 |  |  |  |  |  |  | AutoCommit => 1, | 
| 136 |  |  |  |  |  |  | }; | 
| 137 |  |  |  |  |  |  | } | 
| 138 |  |  |  |  |  |  |  | 
| 139 |  |  |  |  |  |  | # Internal method, used to handle the logic of how to add up return | 
| 140 |  |  |  |  |  |  | #  values from pre_ plugins | 
| 141 |  |  |  |  |  |  | sub handle_pre_plugin_ret { | 
| 142 | 0 |  |  | 0 | 0 | 0 | my ($running_total_ref,$result) = @_; | 
| 143 |  |  |  |  |  |  |  | 
| 144 | 0 | 0 | 0 |  |  | 0 | if(($result && $result == 0) || !$result) { | 
|  |  | 0 | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
| 145 |  |  |  |  |  |  | # No opinion, no need to change things | 
| 146 |  |  |  |  |  |  | } elsif($result == -1 || $result == 1) { | 
| 147 |  |  |  |  |  |  | # Increase or decrease as requested | 
| 148 | 0 |  |  |  |  | 0 | $$running_total_ref += $result; | 
| 149 |  |  |  |  |  |  | } else { | 
| 150 |  |  |  |  |  |  | # Invalid return code | 
| 151 | 0 |  |  |  |  | 0 | warn("Pre_ plugin returned invalid accept/deny value of '$result'"); | 
| 152 |  |  |  |  |  |  | } | 
| 153 |  |  |  |  |  |  | } | 
| 154 |  |  |  |  |  |  |  | 
| 155 |  |  |  |  |  |  | =item B | 
| 156 |  |  |  |  |  |  |  | 
| 157 |  |  |  |  |  |  | my $content = $store->retrieve_node($node); | 
| 158 |  |  |  |  |  |  |  | 
| 159 |  |  |  |  |  |  | # Or get additional meta-data too. | 
| 160 |  |  |  |  |  |  | my %node = $store->retrieve_node("HomePage"); | 
| 161 |  |  |  |  |  |  | print "Current Version: " . $node{version}; | 
| 162 |  |  |  |  |  |  |  | 
| 163 |  |  |  |  |  |  | # Maybe we stored some metadata too. | 
| 164 |  |  |  |  |  |  | my $categories = $node{metadata}{category}; | 
| 165 |  |  |  |  |  |  | print "Categories: " . join(", ", @$categories); | 
| 166 |  |  |  |  |  |  | print "Postcode: $node{metadata}{postcode}[0]"; | 
| 167 |  |  |  |  |  |  |  | 
| 168 |  |  |  |  |  |  | # Or get an earlier version: | 
| 169 |  |  |  |  |  |  | my %node = $store->retrieve_node(name    => "HomePage", | 
| 170 |  |  |  |  |  |  | version => 2 ); | 
| 171 |  |  |  |  |  |  | print $node{content}; | 
| 172 |  |  |  |  |  |  |  | 
| 173 |  |  |  |  |  |  |  | 
| 174 |  |  |  |  |  |  | In scalar context, returns the current (raw Wiki language) contents of | 
| 175 |  |  |  |  |  |  | the specified node. In list context, returns a hash containing the | 
| 176 |  |  |  |  |  |  | contents of the node plus additional data: | 
| 177 |  |  |  |  |  |  |  | 
| 178 |  |  |  |  |  |  | =over 4 | 
| 179 |  |  |  |  |  |  |  | 
| 180 |  |  |  |  |  |  | =item B | 
| 181 |  |  |  |  |  |  |  | 
| 182 |  |  |  |  |  |  | =item B | 
| 183 |  |  |  |  |  |  |  | 
| 184 |  |  |  |  |  |  | =item B | 
| 185 |  |  |  |  |  |  |  | 
| 186 |  |  |  |  |  |  | =item B - a reference to a hash containing any caller-supplied | 
| 187 |  |  |  |  |  |  | metadata sent along the last time the node was written | 
| 188 |  |  |  |  |  |  |  | 
| 189 |  |  |  |  |  |  | =back | 
| 190 |  |  |  |  |  |  |  | 
| 191 |  |  |  |  |  |  | The node parameter is mandatory. The version parameter is optional and | 
| 192 |  |  |  |  |  |  | defaults to the newest version. If the node hasn't been created yet, | 
| 193 |  |  |  |  |  |  | it is considered to exist but be empty (this behaviour might change). | 
| 194 |  |  |  |  |  |  |  | 
| 195 |  |  |  |  |  |  | B on metadata - each hash value is returned as an array ref, | 
| 196 |  |  |  |  |  |  | even if that type of metadata only has one value. | 
| 197 |  |  |  |  |  |  |  | 
| 198 |  |  |  |  |  |  | =cut | 
| 199 |  |  |  |  |  |  |  | 
| 200 |  |  |  |  |  |  | sub retrieve_node { | 
| 201 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 202 | 0 | 0 |  |  |  | 0 | my %args = scalar @_ == 1 ? ( name => $_[0] ) : @_; | 
| 203 | 0 | 0 |  |  |  | 0 | unless($args{'version'}) { $args{'version'} = undef; } | 
|  | 0 |  |  |  |  | 0 |  | 
| 204 |  |  |  |  |  |  |  | 
| 205 |  |  |  |  |  |  | # Call pre_retrieve on any plugins, in case they want to tweak anything | 
| 206 | 0 | 0 |  |  |  | 0 | my @plugins = @{ $args{plugins} || [ ] }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 207 | 0 |  |  |  |  | 0 | foreach my $plugin (@plugins) { | 
| 208 | 0 | 0 |  |  |  | 0 | if ( $plugin->can( "pre_retrieve" ) ) { | 
| 209 |  |  |  |  |  |  | $plugin->pre_retrieve( | 
| 210 |  |  |  |  |  |  | node     => \$args{'name'}, | 
| 211 | 0 |  |  |  |  | 0 | version  => \$args{'version'} | 
| 212 |  |  |  |  |  |  | ); | 
| 213 |  |  |  |  |  |  | } | 
| 214 |  |  |  |  |  |  | } | 
| 215 |  |  |  |  |  |  |  | 
| 216 |  |  |  |  |  |  | # Note _retrieve_node_data is sensitive to calling context. | 
| 217 | 0 | 0 |  |  |  | 0 | unless(wantarray) { | 
| 218 |  |  |  |  |  |  | # Scalar context, will return just the content | 
| 219 | 0 |  |  |  |  | 0 | return $self->_retrieve_node_data( %args ); | 
| 220 |  |  |  |  |  |  | } | 
| 221 |  |  |  |  |  |  |  | 
| 222 | 0 |  |  |  |  | 0 | my %data = $self->_retrieve_node_data( %args ); | 
| 223 | 0 |  |  |  |  | 0 | $data{'checksum'} = $self->_checksum(%data); | 
| 224 | 0 |  |  |  |  | 0 | return %data; | 
| 225 |  |  |  |  |  |  | } | 
| 226 |  |  |  |  |  |  |  | 
| 227 |  |  |  |  |  |  | # Returns hash or scalar depending on calling context. | 
| 228 |  |  |  |  |  |  | sub _retrieve_node_data { | 
| 229 | 0 |  |  | 0 |  | 0 | my ($self, %args) = @_; | 
| 230 | 0 |  |  |  |  | 0 | my %data = $self->_retrieve_node_content( %args ); | 
| 231 | 0 | 0 |  |  |  | 0 | unless(wantarray) { | 
| 232 |  |  |  |  |  |  | # Scalar context, return just the content | 
| 233 | 0 |  |  |  |  | 0 | return $data{content}; | 
| 234 |  |  |  |  |  |  | } | 
| 235 |  |  |  |  |  |  |  | 
| 236 |  |  |  |  |  |  | # If we want additional data then get it.  Note that $data{version} | 
| 237 |  |  |  |  |  |  | # will already have been set by C<_retrieve_node_content>, if it wasn't | 
| 238 |  |  |  |  |  |  | # specified in the call. | 
| 239 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 240 | 0 |  |  |  |  | 0 | my $sql = "SELECT metadata_type, metadata_value " | 
| 241 |  |  |  |  |  |  | . "FROM node " | 
| 242 |  |  |  |  |  |  | . "INNER JOIN metadata ON (node_id = id) " | 
| 243 |  |  |  |  |  |  | . "WHERE name=? " | 
| 244 |  |  |  |  |  |  | . "AND metadata.version=?"; | 
| 245 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare($sql); | 
| 246 | 0 | 0 |  |  |  | 0 | $sth->execute($args{name},$data{version}) or croak $dbh->errstr; | 
| 247 |  |  |  |  |  |  |  | 
| 248 | 0 |  |  |  |  | 0 | my %metadata; | 
| 249 | 0 |  |  |  |  | 0 | while ( my ($type, $val) = $self->charset_decode( $sth->fetchrow_array ) ) { | 
| 250 | 0 | 0 |  |  |  | 0 | if ( defined $metadata{$type} ) { | 
| 251 | 0 |  |  |  |  | 0 | push @{$metadata{$type}}, $val; | 
|  | 0 |  |  |  |  | 0 |  | 
| 252 |  |  |  |  |  |  | } else { | 
| 253 | 0 |  |  |  |  | 0 | $metadata{$type} = [ $val ]; | 
| 254 |  |  |  |  |  |  | } | 
| 255 |  |  |  |  |  |  | } | 
| 256 | 0 |  |  |  |  | 0 | $data{metadata} = \%metadata; | 
| 257 | 0 |  |  |  |  | 0 | return %data; | 
| 258 |  |  |  |  |  |  | } | 
| 259 |  |  |  |  |  |  |  | 
| 260 |  |  |  |  |  |  | # $store->_retrieve_node_content( name    => $node_name, | 
| 261 |  |  |  |  |  |  | #                                 version => $node_version ); | 
| 262 |  |  |  |  |  |  | # Params: 'name' is compulsory, 'version' is optional and defaults to latest. | 
| 263 |  |  |  |  |  |  | # Returns a hash of data for C - content, version, last modified | 
| 264 |  |  |  |  |  |  | sub _retrieve_node_content { | 
| 265 | 0 |  |  | 0 |  | 0 | my ($self, %args) = @_; | 
| 266 | 0 | 0 |  |  |  | 0 | croak "No valid node name supplied" unless $args{name}; | 
| 267 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 268 | 0 |  |  |  |  | 0 | my $sql; | 
| 269 |  |  |  |  |  |  |  | 
| 270 |  |  |  |  |  |  | my $version_sql_val; | 
| 271 | 0 |  |  |  |  | 0 | my $text_source; | 
| 272 | 0 | 0 |  |  |  | 0 | if ( $args{version} ) { | 
| 273 |  |  |  |  |  |  | # Version given - get that version, and the content for that version | 
| 274 | 0 |  |  |  |  | 0 | $version_sql_val = $dbh->quote($self->charset_encode($args{version})); | 
| 275 | 0 |  |  |  |  | 0 | $text_source = "content"; | 
| 276 |  |  |  |  |  |  | } else { | 
| 277 |  |  |  |  |  |  | # No version given, grab latest version (and content for that) | 
| 278 | 0 |  |  |  |  | 0 | $version_sql_val = "node.version"; | 
| 279 | 0 |  |  |  |  | 0 | $text_source = "node"; | 
| 280 |  |  |  |  |  |  | } | 
| 281 |  |  |  |  |  |  | $sql = "SELECT " | 
| 282 |  |  |  |  |  |  | . "     $text_source.text, content.version, " | 
| 283 |  |  |  |  |  |  | . "     content.modified, content.moderated, " | 
| 284 |  |  |  |  |  |  | . "     node.moderate " | 
| 285 |  |  |  |  |  |  | . "FROM node " | 
| 286 |  |  |  |  |  |  | . "INNER JOIN content ON (id = node_id) " | 
| 287 | 0 |  |  |  |  | 0 | . "WHERE name=" . $dbh->quote($self->charset_encode($args{name})) | 
| 288 |  |  |  |  |  |  | . " AND content.version=" . $version_sql_val; | 
| 289 | 0 |  |  |  |  | 0 | my @results = $self->charset_decode( $dbh->selectrow_array($sql) ); | 
| 290 | 0 | 0 |  |  |  | 0 | @results = ("", 0, "") unless scalar @results; | 
| 291 | 0 |  |  |  |  | 0 | my %data; | 
| 292 | 0 |  |  |  |  | 0 | @data{ qw( content version last_modified moderated node_requires_moderation ) } = @results; | 
| 293 | 0 |  |  |  |  | 0 | return %data; | 
| 294 |  |  |  |  |  |  | } | 
| 295 |  |  |  |  |  |  |  | 
| 296 |  |  |  |  |  |  | # Expects a hash as returned by ->retrieve_node - it's actually slightly lax | 
| 297 |  |  |  |  |  |  | # in this, in that while ->retrieve_node always wraps up the metadata values in | 
| 298 |  |  |  |  |  |  | # (refs to) arrays, this method will accept scalar metadata values too. | 
| 299 |  |  |  |  |  |  | sub _checksum { | 
| 300 | 0 |  |  | 0 |  | 0 | my ($self, %node_data) = @_; | 
| 301 | 0 |  |  |  |  | 0 | my $string = $node_data{content}; | 
| 302 | 0 | 0 |  |  |  | 0 | my %metadata = %{ $node_data{metadata} || {} }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 303 | 0 |  |  |  |  | 0 | foreach my $key ( sort keys %metadata ) { | 
| 304 | 0 |  |  |  |  | 0 | $string .= "\0\0\0" . $key . "\0\0"; | 
| 305 | 0 |  |  |  |  | 0 | my $val = $metadata{$key}; | 
| 306 | 0 | 0 |  |  |  | 0 | if ( ref $val eq "ARRAY" ) { | 
| 307 | 0 |  |  |  |  | 0 | $string .= join("\0", sort @$val ); | 
| 308 |  |  |  |  |  |  | } else { | 
| 309 | 0 |  |  |  |  | 0 | $string .= $val; | 
| 310 |  |  |  |  |  |  | } | 
| 311 |  |  |  |  |  |  | } | 
| 312 | 0 |  |  |  |  | 0 | return md5_hex($self->charset_encode($string)); | 
| 313 |  |  |  |  |  |  | } | 
| 314 |  |  |  |  |  |  |  | 
| 315 |  |  |  |  |  |  | # Expects an array of hashes whose keys and values are scalars. | 
| 316 |  |  |  |  |  |  | sub _checksum_hashes { | 
| 317 | 0 |  |  | 0 |  | 0 | my ($self, @hashes) = @_; | 
| 318 | 0 |  |  |  |  | 0 | my @strings = ""; | 
| 319 | 0 |  |  |  |  | 0 | foreach my $hashref ( @hashes ) { | 
| 320 | 0 |  |  |  |  | 0 | my %hash = %$hashref; | 
| 321 | 0 |  |  |  |  | 0 | my $substring = ""; | 
| 322 | 0 |  |  |  |  | 0 | foreach my $key ( sort keys %hash ) { | 
| 323 | 0 |  |  |  |  | 0 | $substring .= "\0\0" . $key . "\0" . $hash{$key}; | 
| 324 |  |  |  |  |  |  | } | 
| 325 | 0 |  |  |  |  | 0 | push @strings, $substring; | 
| 326 |  |  |  |  |  |  | } | 
| 327 | 0 |  |  |  |  | 0 | my $string = join("\0\0\0", sort @strings); | 
| 328 | 0 |  |  |  |  | 0 | return md5_hex($string); | 
| 329 |  |  |  |  |  |  | } | 
| 330 |  |  |  |  |  |  |  | 
| 331 |  |  |  |  |  |  | =item B | 
| 332 |  |  |  |  |  |  |  | 
| 333 |  |  |  |  |  |  | my $ok = $store->node_exists( "Wombat Defenestration" ); | 
| 334 |  |  |  |  |  |  |  | 
| 335 |  |  |  |  |  |  | # or ignore case - optional but recommended | 
| 336 |  |  |  |  |  |  | my $ok = $store->node_exists( | 
| 337 |  |  |  |  |  |  | name        => "monkey brains", | 
| 338 |  |  |  |  |  |  | ignore_case => 1, | 
| 339 |  |  |  |  |  |  | ); | 
| 340 |  |  |  |  |  |  |  | 
| 341 |  |  |  |  |  |  | Returns true if the node has ever been created (even if it is | 
| 342 |  |  |  |  |  |  | currently empty), and false otherwise. | 
| 343 |  |  |  |  |  |  |  | 
| 344 |  |  |  |  |  |  | By default, the case-sensitivity of C depends on your | 
| 345 |  |  |  |  |  |  | database.  If you supply a true value to the C parameter, | 
| 346 |  |  |  |  |  |  | then you can be sure of its being case-insensitive.  This is | 
| 347 |  |  |  |  |  |  | recommended. | 
| 348 |  |  |  |  |  |  |  | 
| 349 |  |  |  |  |  |  | =cut | 
| 350 |  |  |  |  |  |  |  | 
| 351 |  |  |  |  |  |  | sub node_exists { | 
| 352 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 353 | 0 | 0 |  |  |  | 0 | if ( scalar @_ == 1 ) { | 
| 354 | 0 |  |  |  |  | 0 | my $node = shift; | 
| 355 | 0 |  |  |  |  | 0 | return $self->_do_old_node_exists( $node ); | 
| 356 |  |  |  |  |  |  | } else { | 
| 357 | 0 |  |  |  |  | 0 | my %args = @_; | 
| 358 |  |  |  |  |  |  | return $self->_do_old_node_exists( $args{name} ) | 
| 359 | 0 | 0 |  |  |  | 0 | unless $args{ignore_case}; | 
| 360 | 0 |  |  |  |  | 0 | my $sql = $self->_get_node_exists_ignore_case_sql; | 
| 361 | 0 |  |  |  |  | 0 | my $sth = $self->dbh->prepare( $sql ); | 
| 362 | 0 |  |  |  |  | 0 | $sth->execute( $args{name} ); | 
| 363 | 0 |  | 0 |  |  | 0 | my $found_name = $sth->fetchrow_array || ""; | 
| 364 | 0 |  |  |  |  | 0 | $sth->finish; | 
| 365 | 0 | 0 |  |  |  | 0 | return lc($found_name) eq lc($args{name}) ? 1 : 0; | 
| 366 |  |  |  |  |  |  | } | 
| 367 |  |  |  |  |  |  | } | 
| 368 |  |  |  |  |  |  |  | 
| 369 |  |  |  |  |  |  | sub _do_old_node_exists { | 
| 370 | 0 |  |  | 0 |  | 0 | my ($self, $node) = @_; | 
| 371 | 0 | 0 |  |  |  | 0 | my %data = $self->retrieve_node($node) or return (); | 
| 372 | 0 |  |  |  |  | 0 | return $data{version}; # will be 0 if node doesn't exist, >=1 otherwise | 
| 373 |  |  |  |  |  |  | } | 
| 374 |  |  |  |  |  |  |  | 
| 375 |  |  |  |  |  |  | =item B | 
| 376 |  |  |  |  |  |  |  | 
| 377 |  |  |  |  |  |  | my $ok = $store->verify_checksum($node, $checksum); | 
| 378 |  |  |  |  |  |  |  | 
| 379 |  |  |  |  |  |  | Sees whether your checksum is current for the given node. Returns true | 
| 380 |  |  |  |  |  |  | if so, false if not. | 
| 381 |  |  |  |  |  |  |  | 
| 382 |  |  |  |  |  |  | B Be aware that when called directly and without locking, this | 
| 383 |  |  |  |  |  |  | might not be accurate, since there is a small window between the | 
| 384 |  |  |  |  |  |  | checking and the returning where the node might be changed, so | 
| 385 |  |  |  |  |  |  | B rely on it for safe commits; use C for that. It | 
| 386 |  |  |  |  |  |  | can however be useful when previewing edits, for example. | 
| 387 |  |  |  |  |  |  |  | 
| 388 |  |  |  |  |  |  | =cut | 
| 389 |  |  |  |  |  |  |  | 
| 390 |  |  |  |  |  |  | sub verify_checksum { | 
| 391 | 0 |  |  | 0 | 1 | 0 | my ($self, $node, $checksum) = @_; | 
| 392 |  |  |  |  |  |  | #warn $self; | 
| 393 | 0 |  |  |  |  | 0 | my %node_data = $self->_retrieve_node_data( name => $node ); | 
| 394 | 0 |  |  |  |  | 0 | return ( $checksum eq $self->_checksum( %node_data ) ); | 
| 395 |  |  |  |  |  |  | } | 
| 396 |  |  |  |  |  |  |  | 
| 397 |  |  |  |  |  |  | =item B | 
| 398 |  |  |  |  |  |  |  | 
| 399 |  |  |  |  |  |  | # List all nodes that link to the Home Page. | 
| 400 |  |  |  |  |  |  | my @links = $store->list_backlinks( node => "Home Page" ); | 
| 401 |  |  |  |  |  |  |  | 
| 402 |  |  |  |  |  |  | =cut | 
| 403 |  |  |  |  |  |  |  | 
| 404 |  |  |  |  |  |  | sub list_backlinks { | 
| 405 | 0 |  |  | 0 | 1 | 0 | my ( $self, %args ) = @_; | 
| 406 | 0 |  |  |  |  | 0 | my $node = $args{node}; | 
| 407 | 0 | 0 |  |  |  | 0 | croak "Must supply a node name" unless $node; | 
| 408 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 409 |  |  |  |  |  |  | # XXX see comment in list_dangling_links | 
| 410 | 0 |  |  |  |  | 0 | my $sql = "SELECT link_from FROM internal_links INNER JOIN | 
| 411 |  |  |  |  |  |  | node AS node_from ON node_from.name=internal_links.link_from | 
| 412 |  |  |  |  |  |  | WHERE link_to=" | 
| 413 |  |  |  |  |  |  | . $dbh->quote($node); | 
| 414 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare($sql); | 
| 415 | 0 | 0 |  |  |  | 0 | $sth->execute or croak $dbh->errstr; | 
| 416 | 0 |  |  |  |  | 0 | my @backlinks; | 
| 417 | 0 |  |  |  |  | 0 | while ( my ($backlink) = $self->charset_decode( $sth->fetchrow_array ) ) { | 
| 418 | 0 |  |  |  |  | 0 | push @backlinks, $backlink; | 
| 419 |  |  |  |  |  |  | } | 
| 420 | 0 |  |  |  |  | 0 | return @backlinks; | 
| 421 |  |  |  |  |  |  | } | 
| 422 |  |  |  |  |  |  |  | 
| 423 |  |  |  |  |  |  | =item B | 
| 424 |  |  |  |  |  |  |  | 
| 425 |  |  |  |  |  |  | # List all nodes that have been linked to from other nodes but don't | 
| 426 |  |  |  |  |  |  | # yet exist. | 
| 427 |  |  |  |  |  |  | my @links = $store->list_dangling_links; | 
| 428 |  |  |  |  |  |  |  | 
| 429 |  |  |  |  |  |  | Each node is returned once only, regardless of how many other nodes | 
| 430 |  |  |  |  |  |  | link to it. | 
| 431 |  |  |  |  |  |  |  | 
| 432 |  |  |  |  |  |  | =cut | 
| 433 |  |  |  |  |  |  |  | 
| 434 |  |  |  |  |  |  | sub list_dangling_links { | 
| 435 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 436 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 437 |  |  |  |  |  |  | # XXX this is really hiding an inconsistency in the database; | 
| 438 |  |  |  |  |  |  | # should really fix the constraints so that this inconsistency | 
| 439 |  |  |  |  |  |  | # cannot be introduced; also rework this table completely so | 
| 440 |  |  |  |  |  |  | # that it uses IDs, not node names (will simplify rename_node too) | 
| 441 | 0 |  |  |  |  | 0 | my $sql = "SELECT DISTINCT internal_links.link_to | 
| 442 |  |  |  |  |  |  | FROM internal_links INNER JOIN node AS node_from ON | 
| 443 |  |  |  |  |  |  | node_from.name=internal_links.link_from LEFT JOIN node | 
| 444 |  |  |  |  |  |  | AS node_to ON node_to.name=internal_links.link_to | 
| 445 |  |  |  |  |  |  | WHERE node_to.version IS NULL"; | 
| 446 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare($sql); | 
| 447 | 0 | 0 |  |  |  | 0 | $sth->execute or croak $dbh->errstr; | 
| 448 | 0 |  |  |  |  | 0 | my @links; | 
| 449 | 0 |  |  |  |  | 0 | while ( my ($link) = $self->charset_decode( $sth->fetchrow_array ) ) { | 
| 450 | 0 |  |  |  |  | 0 | push @links, $link; | 
| 451 |  |  |  |  |  |  | } | 
| 452 | 0 |  |  |  |  | 0 | return @links; | 
| 453 |  |  |  |  |  |  | } | 
| 454 |  |  |  |  |  |  |  | 
| 455 |  |  |  |  |  |  | =item B | 
| 456 |  |  |  |  |  |  |  | 
| 457 |  |  |  |  |  |  | $store->write_node_post_locking( node     => $node, | 
| 458 |  |  |  |  |  |  | content  => $content, | 
| 459 |  |  |  |  |  |  | links_to => \@links_to, | 
| 460 |  |  |  |  |  |  | metadata => \%metadata, | 
| 461 |  |  |  |  |  |  | requires_moderation => $requires_moderation, | 
| 462 |  |  |  |  |  |  | plugins  => \@plugins   ) | 
| 463 |  |  |  |  |  |  | or handle_error(); | 
| 464 |  |  |  |  |  |  |  | 
| 465 |  |  |  |  |  |  | Writes the specified content into the specified node, then calls | 
| 466 |  |  |  |  |  |  | C on all supplied plugins, with arguments C, | 
| 467 |  |  |  |  |  |  | C, C, C. | 
| 468 |  |  |  |  |  |  |  | 
| 469 |  |  |  |  |  |  | Making sure that locking/unlocking/transactions happen is left up to | 
| 470 |  |  |  |  |  |  | you (or your chosen subclass). This method shouldn't really be used | 
| 471 |  |  |  |  |  |  | directly as it might overwrite someone else's changes. Croaks on error | 
| 472 |  |  |  |  |  |  | but otherwise returns the version number of the update just made.  A | 
| 473 |  |  |  |  |  |  | return value of -1 indicates that the change was not applied.  This | 
| 474 |  |  |  |  |  |  | may be because the plugins voted against the change, or because the | 
| 475 |  |  |  |  |  |  | content and metadata in the proposed new version were identical to the | 
| 476 |  |  |  |  |  |  | current version (a "null" change). | 
| 477 |  |  |  |  |  |  |  | 
| 478 |  |  |  |  |  |  | Supplying a ref to an array of nodes that this ones links to is | 
| 479 |  |  |  |  |  |  | optional, but if you do supply it then this node will be returned when | 
| 480 |  |  |  |  |  |  | calling C on the nodes in C<@links_to>. B that | 
| 481 |  |  |  |  |  |  | if you don't supply the ref then the store will assume that this node | 
| 482 |  |  |  |  |  |  | doesn't link to any others, and update itself accordingly. | 
| 483 |  |  |  |  |  |  |  | 
| 484 |  |  |  |  |  |  | The metadata hashref is also optional, as is requires_moderation. | 
| 485 |  |  |  |  |  |  |  | 
| 486 |  |  |  |  |  |  | B on the metadata hashref: Any data in here that you wish to | 
| 487 |  |  |  |  |  |  | access directly later must be a key-value pair in which the value is | 
| 488 |  |  |  |  |  |  | either a scalar or a reference to an array of scalars.  For example: | 
| 489 |  |  |  |  |  |  |  | 
| 490 |  |  |  |  |  |  | $wiki->write_node( "Calthorpe Arms", "nice pub", $checksum, | 
| 491 |  |  |  |  |  |  | { category => [ "Pubs", "Bloomsbury" ], | 
| 492 |  |  |  |  |  |  | postcode => "WC1X 8JR" } ); | 
| 493 |  |  |  |  |  |  |  | 
| 494 |  |  |  |  |  |  | # and later | 
| 495 |  |  |  |  |  |  |  | 
| 496 |  |  |  |  |  |  | my @nodes = $wiki->list_nodes_by_metadata( | 
| 497 |  |  |  |  |  |  | metadata_type  => "category", | 
| 498 |  |  |  |  |  |  | metadata_value => "Pubs"             ); | 
| 499 |  |  |  |  |  |  |  | 
| 500 |  |  |  |  |  |  | For more advanced usage (passing data through to registered plugins) | 
| 501 |  |  |  |  |  |  | you may if you wish pass key-value pairs in which the value is a | 
| 502 |  |  |  |  |  |  | hashref or an array of hashrefs. The data in the hashrefs will not be | 
| 503 |  |  |  |  |  |  | stored as metadata; it will be checksummed and the checksum will be | 
| 504 |  |  |  |  |  |  | stored instead (as C<__metadatatypename__checksum>). Such data can | 
| 505 |  |  |  |  |  |  | I be accessed via plugins. | 
| 506 |  |  |  |  |  |  |  | 
| 507 |  |  |  |  |  |  | =cut | 
| 508 |  |  |  |  |  |  |  | 
| 509 |  |  |  |  |  |  | sub write_node_post_locking { | 
| 510 | 0 |  |  | 0 | 1 | 0 | my ($self, %args) = @_; | 
| 511 |  |  |  |  |  |  | my ($node, $content, $links_to_ref, $metadata_ref, $requires_moderation) = | 
| 512 | 0 |  |  |  |  | 0 | @args{ qw( node content links_to metadata requires_moderation) }; | 
| 513 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 514 |  |  |  |  |  |  |  | 
| 515 | 0 |  |  |  |  | 0 | my $timestamp = $self->_get_timestamp(); | 
| 516 | 0 | 0 |  |  |  | 0 | my @links_to = @{ $links_to_ref || [] }; # default to empty array | 
|  | 0 |  |  |  |  | 0 |  | 
| 517 | 0 |  |  |  |  | 0 | my $version; | 
| 518 | 0 | 0 |  |  |  | 0 | unless($requires_moderation) { $requires_moderation = 0; } | 
|  | 0 |  |  |  |  | 0 |  | 
| 519 |  |  |  |  |  |  |  | 
| 520 |  |  |  |  |  |  | # Call pre_write on any plugins, in case they want to tweak anything | 
| 521 | 0 | 0 |  |  |  | 0 | my @preplugins = @{ $args{plugins} || [ ] }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 522 | 0 |  |  |  |  | 0 | my $write_allowed = 1; | 
| 523 | 0 |  |  |  |  | 0 | foreach my $plugin (@preplugins) { | 
| 524 | 0 | 0 |  |  |  | 0 | if ( $plugin->can( "pre_write" ) ) { | 
| 525 | 0 |  |  |  |  | 0 | handle_pre_plugin_ret( | 
| 526 |  |  |  |  |  |  | \$write_allowed, | 
| 527 |  |  |  |  |  |  | $plugin->pre_write( | 
| 528 |  |  |  |  |  |  | node     => \$node, | 
| 529 |  |  |  |  |  |  | content  => \$content, | 
| 530 |  |  |  |  |  |  | metadata => \$metadata_ref ) | 
| 531 |  |  |  |  |  |  | ); | 
| 532 |  |  |  |  |  |  | } | 
| 533 |  |  |  |  |  |  | } | 
| 534 | 0 | 0 |  |  |  | 0 | if($write_allowed < 1) { | 
| 535 |  |  |  |  |  |  | # The plugins didn't want to allow this action | 
| 536 | 0 |  |  |  |  | 0 | return -1; | 
| 537 |  |  |  |  |  |  | } | 
| 538 |  |  |  |  |  |  |  | 
| 539 | 0 | 0 |  |  |  | 0 | if ( $self->_checksum( %args ) eq $args{checksum} ) { | 
| 540 |  |  |  |  |  |  | # Refuse to commit as nothing has changed | 
| 541 | 0 |  |  |  |  | 0 | return -1; | 
| 542 |  |  |  |  |  |  | } | 
| 543 |  |  |  |  |  |  |  | 
| 544 |  |  |  |  |  |  | # Either inserting a new page or updating an old one. | 
| 545 | 0 |  |  |  |  | 0 | my $sql = "SELECT count(*) FROM node WHERE name=" . $dbh->quote($node); | 
| 546 | 0 |  | 0 |  |  | 0 | my $exists = @{ $dbh->selectcol_arrayref($sql) }[0] || 0; | 
| 547 |  |  |  |  |  |  |  | 
| 548 |  |  |  |  |  |  |  | 
| 549 |  |  |  |  |  |  | # If it doesn't exist, add it right now | 
| 550 | 0 | 0 |  |  |  | 0 | if(! $exists) { | 
| 551 |  |  |  |  |  |  | # Add in a new version | 
| 552 | 0 |  |  |  |  | 0 | $version = 1; | 
| 553 |  |  |  |  |  |  |  | 
| 554 |  |  |  |  |  |  | # Handle initial moderation | 
| 555 | 0 |  |  |  |  | 0 | my $node_content = $content; | 
| 556 | 0 | 0 |  |  |  | 0 | if($requires_moderation) { | 
| 557 | 0 |  |  |  |  | 0 | $node_content = "=== This page has yet to be moderated. ==="; | 
| 558 |  |  |  |  |  |  | } | 
| 559 |  |  |  |  |  |  |  | 
| 560 |  |  |  |  |  |  | # Add the node and content | 
| 561 | 0 |  |  |  |  | 0 | my $add_sql = | 
| 562 |  |  |  |  |  |  | "INSERT INTO node " | 
| 563 |  |  |  |  |  |  | ."    (name, version, text, modified, moderate) " | 
| 564 |  |  |  |  |  |  | ."VALUES (?, ?, ?, ?, ?)"; | 
| 565 | 0 |  |  |  |  | 0 | my $add_sth = $dbh->prepare($add_sql); | 
| 566 |  |  |  |  |  |  | $add_sth->execute( | 
| 567 | 0 | 0 |  |  |  | 0 | map{ $self->charset_encode($_) } | 
|  | 0 |  |  |  |  | 0 |  | 
| 568 |  |  |  |  |  |  | ($node, $version, $node_content, $timestamp, $requires_moderation) | 
| 569 |  |  |  |  |  |  | ) or croak "Error updating database: " . DBI->errstr; | 
| 570 |  |  |  |  |  |  | } | 
| 571 |  |  |  |  |  |  |  | 
| 572 |  |  |  |  |  |  | # Get the ID of the node we've added / we're about to update | 
| 573 |  |  |  |  |  |  | # Also get the moderation status for it | 
| 574 | 0 |  |  |  |  | 0 | $sql = "SELECT id, moderate FROM node WHERE name=" . $dbh->quote($node); | 
| 575 | 0 |  |  |  |  | 0 | my ($node_id,$node_requires_moderation) = $dbh->selectrow_array($sql); | 
| 576 |  |  |  |  |  |  |  | 
| 577 |  |  |  |  |  |  | # Only update node if it exists, and moderation isn't enabled on the node | 
| 578 |  |  |  |  |  |  | # Whatever happens, if it exists, generate a new version number | 
| 579 | 0 | 0 |  |  |  | 0 | if($exists) { | 
| 580 |  |  |  |  |  |  | # Get the new version number | 
| 581 | 0 |  |  |  |  | 0 | $sql = "SELECT max(content.version) FROM node | 
| 582 |  |  |  |  |  |  | INNER JOIN content ON (id = node_id) | 
| 583 |  |  |  |  |  |  | WHERE name=" . $dbh->quote($node); | 
| 584 | 0 |  | 0 |  |  | 0 | $version = @{ $dbh->selectcol_arrayref($sql) }[0] || 0; | 
| 585 | 0 | 0 |  |  |  | 0 | croak "Can't get version number" unless $version; | 
| 586 | 0 |  |  |  |  | 0 | $version++; | 
| 587 |  |  |  |  |  |  |  | 
| 588 |  |  |  |  |  |  | # Update the node only if node doesn't require moderation | 
| 589 | 0 | 0 |  |  |  | 0 | if(!$node_requires_moderation) { | 
| 590 | 0 |  |  |  |  | 0 | $sql = "UPDATE node SET version=" . $dbh->quote($version) | 
| 591 |  |  |  |  |  |  | . ", text=" . $dbh->quote($self->charset_encode($content)) | 
| 592 |  |  |  |  |  |  | . ", modified=" . $dbh->quote($timestamp) | 
| 593 |  |  |  |  |  |  | . " WHERE name=" . $dbh->quote($self->charset_encode($node)); | 
| 594 | 0 | 0 |  |  |  | 0 | $dbh->do($sql) or croak "Error updating database: " . DBI->errstr; | 
| 595 |  |  |  |  |  |  | } | 
| 596 |  |  |  |  |  |  |  | 
| 597 |  |  |  |  |  |  | # You can't use this to enable moderation on an existing node | 
| 598 | 0 | 0 |  |  |  | 0 | if($requires_moderation) { | 
| 599 | 0 |  |  |  |  | 0 | warn("Moderation not added to existing node '$node', use normal moderation methods instead"); | 
| 600 |  |  |  |  |  |  | } | 
| 601 |  |  |  |  |  |  | } | 
| 602 |  |  |  |  |  |  |  | 
| 603 |  |  |  |  |  |  |  | 
| 604 |  |  |  |  |  |  | # Now node is updated (if required), add to the history | 
| 605 | 0 |  |  |  |  | 0 | my $add_sql = | 
| 606 |  |  |  |  |  |  | "INSERT INTO content " | 
| 607 |  |  |  |  |  |  | ."    (node_id, version, text, modified, moderated) " | 
| 608 |  |  |  |  |  |  | ."VALUES (?, ?, ?, ?, ?)"; | 
| 609 | 0 |  |  |  |  | 0 | my $add_sth = $dbh->prepare($add_sql); | 
| 610 |  |  |  |  |  |  | $add_sth->execute( | 
| 611 | 0 | 0 |  |  |  | 0 | map { $self->charset_encode($_) } | 
|  | 0 |  |  |  |  | 0 |  | 
| 612 |  |  |  |  |  |  | ($node_id, $version, $content, $timestamp, (1-$node_requires_moderation)) | 
| 613 |  |  |  |  |  |  | ) or croak "Error updating database: " . DBI->errstr; | 
| 614 |  |  |  |  |  |  |  | 
| 615 |  |  |  |  |  |  |  | 
| 616 |  |  |  |  |  |  | # Update the backlinks. | 
| 617 | 0 | 0 |  |  |  | 0 | $dbh->do("DELETE FROM internal_links WHERE link_from=" | 
| 618 |  |  |  |  |  |  | . $dbh->quote($self->charset_encode($node)) ) or croak $dbh->errstr; | 
| 619 | 0 |  |  |  |  | 0 | foreach my $links_to ( @links_to ) { | 
| 620 |  |  |  |  |  |  | $sql = "INSERT INTO internal_links (link_from, link_to) VALUES (" | 
| 621 | 0 |  |  |  |  | 0 | . join(", ", map { $dbh->quote($self->charset_encode($_)) } ( $node, $links_to ) ) . ")"; | 
|  | 0 |  |  |  |  | 0 |  | 
| 622 |  |  |  |  |  |  | # Better to drop a backlink or two than to lose the whole update. | 
| 623 |  |  |  |  |  |  | # Shevek wants a case-sensitive wiki, Jerakeen wants a case-insensitive | 
| 624 |  |  |  |  |  |  | # one, MySQL compares case-sensitively on varchars unless you add | 
| 625 |  |  |  |  |  |  | # the binary keyword.  Case-sensitivity to be revisited. | 
| 626 | 0 |  |  |  |  | 0 | eval { $dbh->do($sql); }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 627 | 0 | 0 |  |  |  | 0 | carp "Couldn't index backlink: " . $dbh->errstr if $@; | 
| 628 |  |  |  |  |  |  | } | 
| 629 |  |  |  |  |  |  |  | 
| 630 |  |  |  |  |  |  | # And also store any metadata.  Note that any entries already in the | 
| 631 |  |  |  |  |  |  | # metadata table refer to old versions, so we don't need to delete them. | 
| 632 | 0 | 0 |  |  |  | 0 | my %metadata = %{ $metadata_ref || {} }; # default to no metadata | 
|  | 0 |  |  |  |  | 0 |  | 
| 633 | 0 |  |  |  |  | 0 | foreach my $type ( keys %metadata ) { | 
| 634 | 0 |  |  |  |  | 0 | my $val = $metadata{$type}; | 
| 635 |  |  |  |  |  |  |  | 
| 636 |  |  |  |  |  |  | # We might have one or many values; make an array now to merge cases. | 
| 637 | 0 | 0 | 0 |  |  | 0 | my @values = (ref $val and ref $val eq 'ARRAY') ? @$val : ( $val ); | 
| 638 |  |  |  |  |  |  |  | 
| 639 |  |  |  |  |  |  | # Find out whether all values for this type are scalars. | 
| 640 | 0 |  |  |  |  | 0 | my $all_scalars = 1; | 
| 641 | 0 |  |  |  |  | 0 | foreach my $value (@values) { | 
| 642 | 0 | 0 |  |  |  | 0 | $all_scalars = 0 if ref $value; | 
| 643 |  |  |  |  |  |  | } | 
| 644 |  |  |  |  |  |  |  | 
| 645 |  |  |  |  |  |  | # For adding to metadata | 
| 646 | 0 |  |  |  |  | 0 | my $add_sql = | 
| 647 |  |  |  |  |  |  | "INSERT INTO metadata " | 
| 648 |  |  |  |  |  |  | ."   (node_id, version, metadata_type, metadata_value) " | 
| 649 |  |  |  |  |  |  | ."VALUES (?, ?, ?, ?)"; | 
| 650 | 0 |  |  |  |  | 0 | my $add_sth = $dbh->prepare($add_sql); | 
| 651 |  |  |  |  |  |  |  | 
| 652 |  |  |  |  |  |  | # If all values for this type are scalars, strip out any duplicates | 
| 653 |  |  |  |  |  |  | # and store the data. | 
| 654 | 0 | 0 |  |  |  | 0 | if ( $all_scalars ) { | 
| 655 | 0 |  |  |  |  | 0 | my %unique = map { $_ => 1 } @values; | 
|  | 0 |  |  |  |  | 0 |  | 
| 656 | 0 |  |  |  |  | 0 | @values = keys %unique; | 
| 657 |  |  |  |  |  |  |  | 
| 658 | 0 |  |  |  |  | 0 | foreach my $value ( @values ) { | 
| 659 |  |  |  |  |  |  | $add_sth->execute( | 
| 660 | 0 | 0 |  |  |  | 0 | map { $self->charset_encode($_) } | 
|  | 0 |  |  |  |  | 0 |  | 
| 661 |  |  |  |  |  |  | ( $node_id, $version, $type, $value ) | 
| 662 |  |  |  |  |  |  | ) or croak $dbh->errstr; | 
| 663 |  |  |  |  |  |  | } | 
| 664 |  |  |  |  |  |  | } else { | 
| 665 |  |  |  |  |  |  | # Otherwise grab a checksum and store that. | 
| 666 | 0 |  |  |  |  | 0 | my $type_to_store  = "__" . $type . "__checksum"; | 
| 667 | 0 |  |  |  |  | 0 | my $value_to_store = $self->_checksum_hashes( @values ); | 
| 668 |  |  |  |  |  |  | $add_sth->execute( | 
| 669 | 0 | 0 |  |  |  | 0 | map { $self->charset_encode($_) } | 
|  | 0 |  |  |  |  | 0 |  | 
| 670 |  |  |  |  |  |  | ( $node_id, $version, $type_to_store, $value_to_store ) | 
| 671 |  |  |  |  |  |  | )  or croak $dbh->errstr; | 
| 672 |  |  |  |  |  |  | } | 
| 673 |  |  |  |  |  |  | } | 
| 674 |  |  |  |  |  |  |  | 
| 675 |  |  |  |  |  |  | # Finally call post_write on any plugins. | 
| 676 | 0 | 0 |  |  |  | 0 | my @postplugins = @{ $args{plugins} || [ ] }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 677 | 0 |  |  |  |  | 0 | foreach my $plugin (@postplugins) { | 
| 678 | 0 | 0 |  |  |  | 0 | if ( $plugin->can( "post_write" ) ) { | 
| 679 | 0 |  |  |  |  | 0 | $plugin->post_write( | 
| 680 |  |  |  |  |  |  | node     => $node, | 
| 681 |  |  |  |  |  |  | node_id  => $node_id, | 
| 682 |  |  |  |  |  |  | version  => $version, | 
| 683 |  |  |  |  |  |  | content  => $content, | 
| 684 |  |  |  |  |  |  | metadata => $metadata_ref ); | 
| 685 |  |  |  |  |  |  | } | 
| 686 |  |  |  |  |  |  | } | 
| 687 |  |  |  |  |  |  |  | 
| 688 | 0 |  |  |  |  | 0 | return $version; | 
| 689 |  |  |  |  |  |  | } | 
| 690 |  |  |  |  |  |  |  | 
| 691 |  |  |  |  |  |  | # Returns the timestamp of now, unless epoch is supplied. | 
| 692 |  |  |  |  |  |  | sub _get_timestamp { | 
| 693 | 0 |  |  | 0 |  | 0 | my $self = shift; | 
| 694 |  |  |  |  |  |  | # I don't care about no steenkin' timezones (yet). | 
| 695 | 0 |  | 0 |  |  | 0 | my $time = shift || localtime; # Overloaded by Time::Piece. | 
| 696 | 0 | 0 |  |  |  | 0 | unless( ref $time ) { | 
| 697 | 0 |  |  |  |  | 0 | $time = localtime($time); # Make it into an object for strftime | 
| 698 |  |  |  |  |  |  | } | 
| 699 | 0 |  |  |  |  | 0 | return $time->strftime($timestamp_fmt); # global | 
| 700 |  |  |  |  |  |  | } | 
| 701 |  |  |  |  |  |  |  | 
| 702 |  |  |  |  |  |  | =item B | 
| 703 |  |  |  |  |  |  |  | 
| 704 |  |  |  |  |  |  | $store->rename_node( | 
| 705 |  |  |  |  |  |  | old_name  => $node, | 
| 706 |  |  |  |  |  |  | new_name  => $new_node, | 
| 707 |  |  |  |  |  |  | wiki      => $wiki, | 
| 708 |  |  |  |  |  |  | create_new_versions => $create_new_versions, | 
| 709 |  |  |  |  |  |  | ); | 
| 710 |  |  |  |  |  |  |  | 
| 711 |  |  |  |  |  |  | Renames a node, updating any references to it as required (assuming your | 
| 712 |  |  |  |  |  |  | chosen formatter supports rename, that is). | 
| 713 |  |  |  |  |  |  |  | 
| 714 |  |  |  |  |  |  | Uses the internal_links table to identify the nodes that link to this | 
| 715 |  |  |  |  |  |  | one, and re-writes any wiki links in these to point to the new name. | 
| 716 |  |  |  |  |  |  |  | 
| 717 |  |  |  |  |  |  | =cut | 
| 718 |  |  |  |  |  |  |  | 
| 719 |  |  |  |  |  |  | sub rename_node { | 
| 720 | 0 |  |  | 0 | 1 | 0 | my ($self, %args) = @_; | 
| 721 |  |  |  |  |  |  | my ($old_name,$new_name,$wiki,$create_new_versions) = | 
| 722 | 0 |  |  |  |  | 0 | @args{ qw( old_name new_name wiki create_new_versions ) }; | 
| 723 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 724 | 0 |  |  |  |  | 0 | my $formatter = $wiki->{_formatter}; | 
| 725 |  |  |  |  |  |  |  | 
| 726 |  |  |  |  |  |  | # For formatters that support it, run the new name through the node name | 
| 727 |  |  |  |  |  |  | # to param conversion and back again, to make sure any necessary munging | 
| 728 |  |  |  |  |  |  | # gets done. | 
| 729 | 0 | 0 | 0 |  |  | 0 | if ( $formatter->can( "node_name_to_node_param" ) | 
| 730 |  |  |  |  |  |  | && $formatter->can( "node_param_to_node_name" ) ) { | 
| 731 | 0 |  |  |  |  | 0 | $new_name = $formatter->node_param_to_node_name( | 
| 732 |  |  |  |  |  |  | $formatter->node_name_to_node_param( $new_name ) ); | 
| 733 |  |  |  |  |  |  | } | 
| 734 |  |  |  |  |  |  |  | 
| 735 | 0 |  |  |  |  | 0 | my $timestamp = $self->_get_timestamp(); | 
| 736 |  |  |  |  |  |  |  | 
| 737 |  |  |  |  |  |  | # Call pre_rename on any plugins, in case they want to tweak anything | 
| 738 | 0 | 0 |  |  |  | 0 | my @preplugins = @{ $args{plugins} || [ ] }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 739 | 0 |  |  |  |  | 0 | my $rename_allowed = 1; | 
| 740 | 0 |  |  |  |  | 0 | foreach my $plugin (@preplugins) { | 
| 741 | 0 | 0 |  |  |  | 0 | if ( $plugin->can( "pre_rename" ) ) { | 
| 742 | 0 |  |  |  |  | 0 | handle_pre_plugin_ret( | 
| 743 |  |  |  |  |  |  | \$rename_allowed, | 
| 744 |  |  |  |  |  |  | $plugin->pre_rename( | 
| 745 |  |  |  |  |  |  | old_name => \$old_name, | 
| 746 |  |  |  |  |  |  | new_name => \$new_name, | 
| 747 |  |  |  |  |  |  | create_new_versions => \$create_new_versions, | 
| 748 |  |  |  |  |  |  | ) | 
| 749 |  |  |  |  |  |  | ); | 
| 750 |  |  |  |  |  |  | } | 
| 751 |  |  |  |  |  |  | } | 
| 752 | 0 | 0 |  |  |  | 0 | if($rename_allowed < 1) { | 
| 753 |  |  |  |  |  |  | # The plugins didn't want to allow this action | 
| 754 | 0 |  |  |  |  | 0 | return -1; | 
| 755 |  |  |  |  |  |  | } | 
| 756 |  |  |  |  |  |  |  | 
| 757 |  |  |  |  |  |  | # Get the ID of the node | 
| 758 | 0 |  |  |  |  | 0 | my $sql = "SELECT id FROM node WHERE name=?"; | 
| 759 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare($sql); | 
| 760 | 0 |  |  |  |  | 0 | $sth->execute($old_name); | 
| 761 | 0 |  |  |  |  | 0 | my ($node_id) = $sth->fetchrow_array; | 
| 762 | 0 |  |  |  |  | 0 | $sth->finish; | 
| 763 |  |  |  |  |  |  |  | 
| 764 |  |  |  |  |  |  |  | 
| 765 |  |  |  |  |  |  | # If the formatter supports it, get a list of the internal | 
| 766 |  |  |  |  |  |  | #  links to the page, which will have their links re-written | 
| 767 |  |  |  |  |  |  | # (Do now before we update the name of the node, in case of | 
| 768 |  |  |  |  |  |  | #  self links) | 
| 769 | 0 |  |  |  |  | 0 | my @links; | 
| 770 | 0 | 0 |  |  |  | 0 | if($formatter->can("rename_links")) { | 
| 771 |  |  |  |  |  |  | # Get a list of the pages that link to the page | 
| 772 | 0 |  |  |  |  | 0 | $sql = "SELECT id, name, version " | 
| 773 |  |  |  |  |  |  | ."FROM internal_links " | 
| 774 |  |  |  |  |  |  | ."INNER JOIN node " | 
| 775 |  |  |  |  |  |  | ."    ON (link_from = name) " | 
| 776 |  |  |  |  |  |  | ."WHERE link_to = ?"; | 
| 777 | 0 |  |  |  |  | 0 | $sth = $dbh->prepare($sql); | 
| 778 | 0 |  |  |  |  | 0 | $sth->execute($old_name); | 
| 779 |  |  |  |  |  |  |  | 
| 780 |  |  |  |  |  |  | # Grab them all, then update, so no locking problems | 
| 781 | 0 |  |  |  |  | 0 | while(my @l = $sth->fetchrow_array) { push (@links, \@l); } | 
|  | 0 |  |  |  |  | 0 |  | 
| 782 |  |  |  |  |  |  | } | 
| 783 |  |  |  |  |  |  |  | 
| 784 |  |  |  |  |  |  |  | 
| 785 |  |  |  |  |  |  | # Rename the node | 
| 786 | 0 |  |  |  |  | 0 | $sql = "UPDATE node SET name=? WHERE id=?"; | 
| 787 | 0 |  |  |  |  | 0 | $sth = $dbh->prepare($sql); | 
| 788 | 0 |  |  |  |  | 0 | $sth->execute($new_name,$node_id); | 
| 789 |  |  |  |  |  |  |  | 
| 790 |  |  |  |  |  |  |  | 
| 791 |  |  |  |  |  |  | # Fix the internal links from this page | 
| 792 |  |  |  |  |  |  | # (Otherwise write_node will get confused if we rename links later on) | 
| 793 | 0 |  |  |  |  | 0 | $sql = "UPDATE internal_links SET link_from=? WHERE link_from=?"; | 
| 794 | 0 |  |  |  |  | 0 | $sth = $dbh->prepare($sql); | 
| 795 | 0 |  |  |  |  | 0 | $sth->execute($new_name,$old_name); | 
| 796 |  |  |  |  |  |  |  | 
| 797 |  |  |  |  |  |  |  | 
| 798 |  |  |  |  |  |  | # Update the text of internal links, if the formatter supports it | 
| 799 | 0 | 0 |  |  |  | 0 | if($formatter->can("rename_links")) { | 
| 800 |  |  |  |  |  |  | # Update the linked pages (may include renamed page) | 
| 801 | 0 |  |  |  |  | 0 | foreach my $l (@links) { | 
| 802 | 0 |  |  |  |  | 0 | my ($page_id, $page_name, $page_version) = @$l; | 
| 803 |  |  |  |  |  |  | # Self link special case | 
| 804 | 0 | 0 |  |  |  | 0 | if($page_name eq $old_name) { $page_name = $new_name; } | 
|  | 0 |  |  |  |  | 0 |  | 
| 805 |  |  |  |  |  |  |  | 
| 806 |  |  |  |  |  |  | # Grab the latest version of that page | 
| 807 | 0 |  |  |  |  | 0 | my %page = $self->retrieve_node( | 
| 808 |  |  |  |  |  |  | name=>$page_name, version=>$page_version | 
| 809 |  |  |  |  |  |  | ); | 
| 810 |  |  |  |  |  |  |  | 
| 811 |  |  |  |  |  |  | # Update the content of the page | 
| 812 |  |  |  |  |  |  | my $new_content = | 
| 813 | 0 |  |  |  |  | 0 | $formatter->rename_links($old_name,$new_name,$page{'content'}); | 
| 814 |  |  |  |  |  |  |  | 
| 815 |  |  |  |  |  |  | # Did it change? | 
| 816 | 0 | 0 |  |  |  | 0 | if($new_content ne $page{'content'}) { | 
| 817 |  |  |  |  |  |  | # Write the updated page out | 
| 818 | 0 | 0 |  |  |  | 0 | if($create_new_versions) { | 
| 819 |  |  |  |  |  |  | # Write out as a new version of the node | 
| 820 |  |  |  |  |  |  | # (This will also fix our internal links) | 
| 821 |  |  |  |  |  |  | $wiki->write_node( | 
| 822 |  |  |  |  |  |  | $page_name, | 
| 823 |  |  |  |  |  |  | $new_content, | 
| 824 |  |  |  |  |  |  | $page{checksum}, | 
| 825 |  |  |  |  |  |  | $page{metadata} | 
| 826 | 0 |  |  |  |  | 0 | ); | 
| 827 |  |  |  |  |  |  | } else { | 
| 828 |  |  |  |  |  |  | # Just update the content | 
| 829 | 0 |  |  |  |  | 0 | my $update_sql_a = "UPDATE node SET text=? WHERE id=?"; | 
| 830 | 0 |  |  |  |  | 0 | my $update_sql_b = "UPDATE content SET text=? ". | 
| 831 |  |  |  |  |  |  | "WHERE node_id=? AND version=?"; | 
| 832 |  |  |  |  |  |  |  | 
| 833 | 0 |  |  |  |  | 0 | my $u_sth = $dbh->prepare($update_sql_a); | 
| 834 | 0 |  |  |  |  | 0 | $u_sth->execute($new_content,$page_id); | 
| 835 | 0 |  |  |  |  | 0 | $u_sth = $dbh->prepare($update_sql_b); | 
| 836 | 0 |  |  |  |  | 0 | $u_sth->execute($new_content,$page_id,$page_version); | 
| 837 |  |  |  |  |  |  | } | 
| 838 |  |  |  |  |  |  | } | 
| 839 |  |  |  |  |  |  | } | 
| 840 |  |  |  |  |  |  |  | 
| 841 |  |  |  |  |  |  | # Fix the internal links if we didn't create new versions of the node | 
| 842 | 0 | 0 |  |  |  | 0 | if(! $create_new_versions) { | 
| 843 | 0 |  |  |  |  | 0 | $sql = "UPDATE internal_links SET link_to=? WHERE link_to=?"; | 
| 844 | 0 |  |  |  |  | 0 | $sth = $dbh->prepare($sql); | 
| 845 | 0 |  |  |  |  | 0 | $sth->execute($new_name,$old_name); | 
| 846 |  |  |  |  |  |  | } | 
| 847 |  |  |  |  |  |  | } else { | 
| 848 | 0 |  |  |  |  | 0 | warn("Internal links not updated following node rename - unsupported by formatter"); | 
| 849 |  |  |  |  |  |  | } | 
| 850 |  |  |  |  |  |  |  | 
| 851 |  |  |  |  |  |  | # Call post_rename on any plugins, in case they want to do anything | 
| 852 | 0 | 0 |  |  |  | 0 | my @postplugins = @{ $args{plugins} || [ ] }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 853 | 0 |  |  |  |  | 0 | foreach my $plugin (@postplugins) { | 
| 854 | 0 | 0 |  |  |  | 0 | if ( $plugin->can( "post_rename" ) ) { | 
| 855 | 0 |  |  |  |  | 0 | $plugin->post_rename( | 
| 856 |  |  |  |  |  |  | old_name => $old_name, | 
| 857 |  |  |  |  |  |  | new_name => $new_name, | 
| 858 |  |  |  |  |  |  | node_id => $node_id, | 
| 859 |  |  |  |  |  |  | ); | 
| 860 |  |  |  |  |  |  | } | 
| 861 |  |  |  |  |  |  | } | 
| 862 |  |  |  |  |  |  | } | 
| 863 |  |  |  |  |  |  |  | 
| 864 |  |  |  |  |  |  | =item B | 
| 865 |  |  |  |  |  |  |  | 
| 866 |  |  |  |  |  |  | $store->moderate_node( | 
| 867 |  |  |  |  |  |  | name    => $node, | 
| 868 |  |  |  |  |  |  | version => $version | 
| 869 |  |  |  |  |  |  | ); | 
| 870 |  |  |  |  |  |  |  | 
| 871 |  |  |  |  |  |  | Marks the given version of the node as moderated. If this is the | 
| 872 |  |  |  |  |  |  | highest moderated version, then update the node's contents to hold | 
| 873 |  |  |  |  |  |  | this version. | 
| 874 |  |  |  |  |  |  |  | 
| 875 |  |  |  |  |  |  | =cut | 
| 876 |  |  |  |  |  |  |  | 
| 877 |  |  |  |  |  |  | sub moderate_node { | 
| 878 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 879 | 0 | 0 |  |  |  | 0 | my %args = scalar @_ == 2 ? ( name => $_[0], version => $_[1] ) : @_; | 
| 880 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 881 |  |  |  |  |  |  |  | 
| 882 | 0 |  |  |  |  | 0 | my ($name,$version) = ($args{name},$args{version}); | 
| 883 |  |  |  |  |  |  |  | 
| 884 |  |  |  |  |  |  | # Call pre_moderate on any plugins. | 
| 885 | 0 | 0 |  |  |  | 0 | my @plugins = @{ $args{plugins} || [ ] }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 886 | 0 |  |  |  |  | 0 | my $moderation_allowed = 1; | 
| 887 | 0 |  |  |  |  | 0 | foreach my $plugin (@plugins) { | 
| 888 | 0 | 0 |  |  |  | 0 | if ( $plugin->can( "pre_moderate" ) ) { | 
| 889 | 0 |  |  |  |  | 0 | handle_pre_plugin_ret( | 
| 890 |  |  |  |  |  |  | \$moderation_allowed, | 
| 891 |  |  |  |  |  |  | $plugin->pre_moderate( | 
| 892 |  |  |  |  |  |  | node     => \$name, | 
| 893 |  |  |  |  |  |  | version  => \$version ) | 
| 894 |  |  |  |  |  |  | ); | 
| 895 |  |  |  |  |  |  | } | 
| 896 |  |  |  |  |  |  | } | 
| 897 | 0 | 0 |  |  |  | 0 | if($moderation_allowed < 1) { | 
| 898 |  |  |  |  |  |  | # The plugins didn't want to allow this action | 
| 899 | 0 |  |  |  |  | 0 | return -1; | 
| 900 |  |  |  |  |  |  | } | 
| 901 |  |  |  |  |  |  |  | 
| 902 |  |  |  |  |  |  | # Get the ID of this node | 
| 903 | 0 |  |  |  |  | 0 | my $id_sql = "SELECT id FROM node WHERE name=?"; | 
| 904 | 0 |  |  |  |  | 0 | my $id_sth = $dbh->prepare($id_sql); | 
| 905 | 0 |  |  |  |  | 0 | $id_sth->execute($name); | 
| 906 | 0 |  |  |  |  | 0 | my ($node_id) = $id_sth->fetchrow_array; | 
| 907 | 0 |  |  |  |  | 0 | $id_sth->finish; | 
| 908 |  |  |  |  |  |  |  | 
| 909 |  |  |  |  |  |  | # Check what the current highest moderated version is | 
| 910 | 0 |  |  |  |  | 0 | my $hv_sql = | 
| 911 |  |  |  |  |  |  | "SELECT max(version) " | 
| 912 |  |  |  |  |  |  | ."FROM content " | 
| 913 |  |  |  |  |  |  | ."WHERE node_id = ? " | 
| 914 |  |  |  |  |  |  | ."AND moderated = ?"; | 
| 915 | 0 |  |  |  |  | 0 | my $hv_sth = $dbh->prepare($hv_sql); | 
| 916 | 0 | 0 |  |  |  | 0 | $hv_sth->execute($node_id, "1") or croak $dbh->errstr; | 
| 917 | 0 |  |  |  |  | 0 | my ($highest_mod_version) = $hv_sth->fetchrow_array; | 
| 918 | 0 |  |  |  |  | 0 | $hv_sth->finish; | 
| 919 | 0 | 0 |  |  |  | 0 | unless($highest_mod_version) { $highest_mod_version = 0; } | 
|  | 0 |  |  |  |  | 0 |  | 
| 920 |  |  |  |  |  |  |  | 
| 921 |  |  |  |  |  |  | # Mark this version as moderated | 
| 922 | 0 |  |  |  |  | 0 | my $update_sql = | 
| 923 |  |  |  |  |  |  | "UPDATE content " | 
| 924 |  |  |  |  |  |  | ."SET moderated = ? " | 
| 925 |  |  |  |  |  |  | ."WHERE node_id = ? " | 
| 926 |  |  |  |  |  |  | ."AND version = ?"; | 
| 927 | 0 |  |  |  |  | 0 | my $update_sth = $dbh->prepare($update_sql); | 
| 928 | 0 | 0 |  |  |  | 0 | $update_sth->execute("1", $node_id, $version) or croak $dbh->errstr; | 
| 929 |  |  |  |  |  |  |  | 
| 930 |  |  |  |  |  |  | # Are we now the highest moderated version? | 
| 931 | 0 | 0 |  |  |  | 0 | if(int($version) > int($highest_mod_version)) { | 
| 932 |  |  |  |  |  |  | # Newly moderated version is newer than previous moderated version | 
| 933 |  |  |  |  |  |  | # So, make the current version the latest version | 
| 934 | 0 |  |  |  |  | 0 | my %new_data = $self->retrieve_node( name => $name, version => $version ); | 
| 935 |  |  |  |  |  |  |  | 
| 936 |  |  |  |  |  |  | # Make sure last modified is properly null, if not set | 
| 937 | 0 | 0 |  |  |  | 0 | unless($new_data{last_modified}) { $new_data{last_modified} = undef; } | 
|  | 0 |  |  |  |  | 0 |  | 
| 938 |  |  |  |  |  |  |  | 
| 939 | 0 |  |  |  |  | 0 | my $newv_sql = | 
| 940 |  |  |  |  |  |  | "UPDATE node " | 
| 941 |  |  |  |  |  |  | ."SET version=?, text=?, modified=? " | 
| 942 |  |  |  |  |  |  | ."WHERE id = ?"; | 
| 943 | 0 |  |  |  |  | 0 | my $newv_sth = $dbh->prepare($newv_sql); | 
| 944 |  |  |  |  |  |  | $newv_sth->execute( | 
| 945 |  |  |  |  |  |  | $version, $self->charset_encode($new_data{content}), | 
| 946 | 0 | 0 |  |  |  | 0 | $new_data{last_modified}, $node_id | 
| 947 |  |  |  |  |  |  | ) or croak $dbh->errstr; | 
| 948 |  |  |  |  |  |  | } else { | 
| 949 |  |  |  |  |  |  | # A higher version is already moderated, so don't change node | 
| 950 |  |  |  |  |  |  | } | 
| 951 |  |  |  |  |  |  |  | 
| 952 |  |  |  |  |  |  | # TODO: Do something about internal links, if required | 
| 953 |  |  |  |  |  |  |  | 
| 954 |  |  |  |  |  |  | # Finally call post_moderate on any plugins. | 
| 955 | 0 | 0 |  |  |  | 0 | @plugins = @{ $args{plugins} || [ ] }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 956 | 0 |  |  |  |  | 0 | foreach my $plugin (@plugins) { | 
| 957 | 0 | 0 |  |  |  | 0 | if ( $plugin->can( "post_moderate" ) ) { | 
| 958 | 0 |  |  |  |  | 0 | $plugin->post_moderate( | 
| 959 |  |  |  |  |  |  | node     => $name, | 
| 960 |  |  |  |  |  |  | node_id  => $node_id, | 
| 961 |  |  |  |  |  |  | version  => $version ); | 
| 962 |  |  |  |  |  |  | } | 
| 963 |  |  |  |  |  |  | } | 
| 964 |  |  |  |  |  |  |  | 
| 965 | 0 |  |  |  |  | 0 | return 1; | 
| 966 |  |  |  |  |  |  | } | 
| 967 |  |  |  |  |  |  |  | 
| 968 |  |  |  |  |  |  | =item B | 
| 969 |  |  |  |  |  |  |  | 
| 970 |  |  |  |  |  |  | $store->set_node_moderation( | 
| 971 |  |  |  |  |  |  | name     => $node, | 
| 972 |  |  |  |  |  |  | required => $required | 
| 973 |  |  |  |  |  |  | ); | 
| 974 |  |  |  |  |  |  |  | 
| 975 |  |  |  |  |  |  | Sets if new node versions will require moderation or not | 
| 976 |  |  |  |  |  |  |  | 
| 977 |  |  |  |  |  |  | =cut | 
| 978 |  |  |  |  |  |  |  | 
| 979 |  |  |  |  |  |  | sub set_node_moderation { | 
| 980 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 981 | 0 | 0 |  |  |  | 0 | my %args = scalar @_ == 2 ? ( name => $_[0], required => $_[1] ) : @_; | 
| 982 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 983 |  |  |  |  |  |  |  | 
| 984 | 0 |  |  |  |  | 0 | my ($name,$required) = ($args{name},$args{required}); | 
| 985 |  |  |  |  |  |  |  | 
| 986 |  |  |  |  |  |  | # Get the ID of this node | 
| 987 | 0 |  |  |  |  | 0 | my $id_sql = "SELECT id FROM node WHERE name=?"; | 
| 988 | 0 |  |  |  |  | 0 | my $id_sth = $dbh->prepare($id_sql); | 
| 989 | 0 |  |  |  |  | 0 | $id_sth->execute($name); | 
| 990 | 0 |  |  |  |  | 0 | my ($node_id) = $id_sth->fetchrow_array; | 
| 991 | 0 |  |  |  |  | 0 | $id_sth->finish; | 
| 992 |  |  |  |  |  |  |  | 
| 993 |  |  |  |  |  |  | # Check we really got an ID | 
| 994 | 0 | 0 |  |  |  | 0 | unless($node_id) { | 
| 995 | 0 |  |  |  |  | 0 | return 0; | 
| 996 |  |  |  |  |  |  | } | 
| 997 |  |  |  |  |  |  |  | 
| 998 |  |  |  |  |  |  | # Mark it as requiring / not requiring moderation | 
| 999 | 0 |  |  |  |  | 0 | my $mod_sql = | 
| 1000 |  |  |  |  |  |  | "UPDATE node " | 
| 1001 |  |  |  |  |  |  | ."SET moderate = ? " | 
| 1002 |  |  |  |  |  |  | ."WHERE id = ? "; | 
| 1003 | 0 |  |  |  |  | 0 | my $mod_sth = $dbh->prepare($mod_sql); | 
| 1004 | 0 | 0 |  |  |  | 0 | $mod_sth->execute("$required", $node_id) or croak $dbh->errstr; | 
| 1005 |  |  |  |  |  |  |  | 
| 1006 | 0 |  |  |  |  | 0 | return 1; | 
| 1007 |  |  |  |  |  |  | } | 
| 1008 |  |  |  |  |  |  |  | 
| 1009 |  |  |  |  |  |  | =item B | 
| 1010 |  |  |  |  |  |  |  | 
| 1011 |  |  |  |  |  |  | $store->delete_node( | 
| 1012 |  |  |  |  |  |  | name    => $node, | 
| 1013 |  |  |  |  |  |  | version => $version, | 
| 1014 |  |  |  |  |  |  | wiki    => $wiki | 
| 1015 |  |  |  |  |  |  | ); | 
| 1016 |  |  |  |  |  |  |  | 
| 1017 |  |  |  |  |  |  | C is optional.  If it is supplied then only that version of | 
| 1018 |  |  |  |  |  |  | the node will be deleted.  Otherwise the node and all its history will | 
| 1019 |  |  |  |  |  |  | be completely deleted. | 
| 1020 |  |  |  |  |  |  |  | 
| 1021 |  |  |  |  |  |  | C is also optional, but if you care about updating the backlinks | 
| 1022 |  |  |  |  |  |  | you want to include it. | 
| 1023 |  |  |  |  |  |  |  | 
| 1024 |  |  |  |  |  |  | Again, doesn't do any locking. You probably don't want to let anyone | 
| 1025 |  |  |  |  |  |  | except Wiki admins call this. You may not want to use it at all. | 
| 1026 |  |  |  |  |  |  |  | 
| 1027 |  |  |  |  |  |  | Croaks on error, silently does nothing if the node or version doesn't | 
| 1028 |  |  |  |  |  |  | exist, returns true if no error. | 
| 1029 |  |  |  |  |  |  |  | 
| 1030 |  |  |  |  |  |  | =cut | 
| 1031 |  |  |  |  |  |  |  | 
| 1032 |  |  |  |  |  |  | sub delete_node { | 
| 1033 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 1034 |  |  |  |  |  |  | # Backwards compatibility. | 
| 1035 | 0 | 0 |  |  |  | 0 | my %args = ( scalar @_ == 1 ) ? ( name => $_[0] ) : @_; | 
| 1036 |  |  |  |  |  |  |  | 
| 1037 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 1038 | 0 |  |  |  |  | 0 | my ($name, $version, $wiki) = @args{ qw( name version wiki ) }; | 
| 1039 |  |  |  |  |  |  |  | 
| 1040 |  |  |  |  |  |  | # Grab the ID of this node | 
| 1041 |  |  |  |  |  |  | # (It will only ever have one entry in node, but might have entries | 
| 1042 |  |  |  |  |  |  | #  for other versions in metadata and content) | 
| 1043 | 0 |  |  |  |  | 0 | my $id_sql = "SELECT id FROM node WHERE name=?"; | 
| 1044 | 0 |  |  |  |  | 0 | my $id_sth = $dbh->prepare($id_sql); | 
| 1045 | 0 |  |  |  |  | 0 | $id_sth->execute($name); | 
| 1046 | 0 |  |  |  |  | 0 | my ($node_id) = $id_sth->fetchrow_array; | 
| 1047 | 0 |  |  |  |  | 0 | $id_sth->finish; | 
| 1048 |  |  |  |  |  |  |  | 
| 1049 |  |  |  |  |  |  | # Trivial case - delete the whole node and all its history. | 
| 1050 | 0 | 0 |  |  |  | 0 | unless ( $version ) { | 
| 1051 | 0 |  |  |  |  | 0 | my $sql; | 
| 1052 |  |  |  |  |  |  | # Should start a transaction here.  FIXME. | 
| 1053 |  |  |  |  |  |  | # Do deletes | 
| 1054 | 0 |  |  |  |  | 0 | $sql = "DELETE FROM content WHERE node_id = $node_id"; | 
| 1055 | 0 | 0 |  |  |  | 0 | $dbh->do($sql) or croak "Deletion failed: " . DBI->errstr; | 
| 1056 | 0 |  |  |  |  | 0 | $sql = "DELETE FROM internal_links WHERE link_from=".$dbh->quote($name); | 
| 1057 | 0 | 0 |  |  |  | 0 | $dbh->do($sql) or croak $dbh->errstr; | 
| 1058 | 0 |  |  |  |  | 0 | $sql = "DELETE FROM metadata WHERE node_id = $node_id"; | 
| 1059 | 0 | 0 |  |  |  | 0 | $dbh->do($sql) or croak $dbh->errstr; | 
| 1060 | 0 |  |  |  |  | 0 | $sql = "DELETE FROM node WHERE id = $node_id"; | 
| 1061 | 0 | 0 |  |  |  | 0 | $dbh->do($sql) or croak "Deletion failed: " . DBI->errstr; | 
| 1062 |  |  |  |  |  |  |  | 
| 1063 |  |  |  |  |  |  | # And finish it here. | 
| 1064 | 0 |  |  |  |  | 0 | post_delete_node($name,$node_id,$version,$args{plugins}); | 
| 1065 | 0 |  |  |  |  | 0 | return 1; | 
| 1066 |  |  |  |  |  |  | } | 
| 1067 |  |  |  |  |  |  |  | 
| 1068 |  |  |  |  |  |  | # Skip out early if we're trying to delete a nonexistent version. | 
| 1069 | 0 |  |  |  |  | 0 | my %verdata = $self->retrieve_node( name => $name, version => $version ); | 
| 1070 | 0 | 0 |  |  |  | 0 | unless($verdata{version}) { | 
| 1071 | 0 |  |  |  |  | 0 | warn( "Asked to delete nonexistent version $version of node " | 
| 1072 |  |  |  |  |  |  | . "$node_id ($name)" ); | 
| 1073 | 0 |  |  |  |  | 0 | return 1; | 
| 1074 |  |  |  |  |  |  | } | 
| 1075 |  |  |  |  |  |  |  | 
| 1076 |  |  |  |  |  |  | # Reduce to trivial case if deleting the only version. | 
| 1077 | 0 |  |  |  |  | 0 | my $sql = "SELECT COUNT(*) FROM content WHERE node_id = $node_id"; | 
| 1078 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare( $sql ); | 
| 1079 | 0 | 0 |  |  |  | 0 | $sth->execute() or croak "Deletion failed: " . $dbh->errstr; | 
| 1080 | 0 |  |  |  |  | 0 | my ($count) = $sth->fetchrow_array; | 
| 1081 | 0 |  |  |  |  | 0 | $sth->finish; | 
| 1082 | 0 | 0 |  |  |  | 0 | if($count == 1) { | 
| 1083 |  |  |  |  |  |  | # Only one version, so can do the non version delete | 
| 1084 | 0 |  |  |  |  | 0 | return $self->delete_node( name=>$name, plugins=>$args{plugins} ); | 
| 1085 |  |  |  |  |  |  | } | 
| 1086 |  |  |  |  |  |  |  | 
| 1087 |  |  |  |  |  |  | # Check whether we're deleting the latest (moderated) version. | 
| 1088 | 0 |  |  |  |  | 0 | my %currdata = $self->retrieve_node( name => $name ); | 
| 1089 | 0 | 0 |  |  |  | 0 | if ( $currdata{version} == $version ) { | 
| 1090 |  |  |  |  |  |  | # Deleting latest version, so need to update the copy in node | 
| 1091 |  |  |  |  |  |  | # (Can't just grab version ($version - 1) since it may have been | 
| 1092 |  |  |  |  |  |  | #  deleted itself, or might not be moderated.) | 
| 1093 | 0 |  |  |  |  | 0 | my $try = $version - 1; | 
| 1094 | 0 |  |  |  |  | 0 | my %prevdata; | 
| 1095 | 0 |  | 0 |  |  | 0 | until ( $prevdata{version} && $prevdata{moderated} ) { | 
| 1096 | 0 |  |  |  |  | 0 | %prevdata = $self->retrieve_node( | 
| 1097 |  |  |  |  |  |  | name    => $name, | 
| 1098 |  |  |  |  |  |  | version => $try, | 
| 1099 |  |  |  |  |  |  | ); | 
| 1100 | 0 |  |  |  |  | 0 | $try--; | 
| 1101 |  |  |  |  |  |  | } | 
| 1102 |  |  |  |  |  |  |  | 
| 1103 |  |  |  |  |  |  | # Move to new (old) version | 
| 1104 | 0 |  |  |  |  | 0 | my $sql="UPDATE node | 
| 1105 |  |  |  |  |  |  | SET version=?, text=?, modified=? | 
| 1106 |  |  |  |  |  |  | WHERE name=?"; | 
| 1107 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare( $sql ); | 
| 1108 | 0 | 0 |  |  |  | 0 | $sth->execute( @prevdata{ qw( version content last_modified ) }, $name) | 
| 1109 |  |  |  |  |  |  | or croak "Deletion failed: " . $dbh->errstr; | 
| 1110 |  |  |  |  |  |  |  | 
| 1111 |  |  |  |  |  |  | # Remove the current version from content | 
| 1112 | 0 |  |  |  |  | 0 | $sql = "DELETE FROM content | 
| 1113 |  |  |  |  |  |  | WHERE node_id = $node_id | 
| 1114 |  |  |  |  |  |  | AND version = $version"; | 
| 1115 | 0 |  |  |  |  | 0 | $sth = $dbh->prepare( $sql ); | 
| 1116 | 0 | 0 |  |  |  | 0 | $sth->execute() | 
| 1117 |  |  |  |  |  |  | or croak "Deletion failed: " . $dbh->errstr; | 
| 1118 |  |  |  |  |  |  |  | 
| 1119 |  |  |  |  |  |  | # Update the internal links to reflect the new version | 
| 1120 | 0 |  |  |  |  | 0 | $sql = "DELETE FROM internal_links WHERE link_from=?"; | 
| 1121 | 0 |  |  |  |  | 0 | $sth = $dbh->prepare( $sql ); | 
| 1122 | 0 | 0 |  |  |  | 0 | $sth->execute( $name ) | 
| 1123 |  |  |  |  |  |  | or croak "Deletion failed: " . $dbh->errstr; | 
| 1124 | 0 |  |  |  |  | 0 | my @links_to; | 
| 1125 | 0 |  |  |  |  | 0 | my $formatter = $wiki->formatter; | 
| 1126 | 0 | 0 |  |  |  | 0 | if ( $formatter->can( "find_internal_links" ) ) { | 
| 1127 |  |  |  |  |  |  | # Supply $metadata to formatter in case it's needed to alter the | 
| 1128 |  |  |  |  |  |  | # behaviour of the formatter, eg for Wiki::Toolkit::Formatter::Multiple | 
| 1129 |  |  |  |  |  |  | my @all = $formatter->find_internal_links( | 
| 1130 | 0 |  |  |  |  | 0 | $prevdata{content}, $prevdata{metadata} ); | 
| 1131 | 0 |  |  |  |  | 0 | my %unique = map { $_ => 1 } @all; | 
|  | 0 |  |  |  |  | 0 |  | 
| 1132 | 0 |  |  |  |  | 0 | @links_to = keys %unique; | 
| 1133 |  |  |  |  |  |  | } | 
| 1134 | 0 |  |  |  |  | 0 | $sql = "INSERT INTO internal_links (link_from, link_to) VALUES (?,?)"; | 
| 1135 | 0 |  |  |  |  | 0 | $sth = $dbh->prepare( $sql ); | 
| 1136 | 0 |  |  |  |  | 0 | foreach my $link ( @links_to ) { | 
| 1137 | 0 |  |  |  |  | 0 | eval { $sth->execute( $name, $link ); }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 1138 | 0 | 0 |  |  |  | 0 | carp "Couldn't index backlink: " . $dbh->errstr if $@; | 
| 1139 |  |  |  |  |  |  | } | 
| 1140 |  |  |  |  |  |  |  | 
| 1141 |  |  |  |  |  |  | # Delete the metadata for the old version | 
| 1142 | 0 |  |  |  |  | 0 | $sql = "DELETE FROM metadata | 
| 1143 |  |  |  |  |  |  | WHERE node_id = $node_id | 
| 1144 |  |  |  |  |  |  | AND version = $version"; | 
| 1145 | 0 |  |  |  |  | 0 | $sth = $dbh->prepare( $sql ); | 
| 1146 | 0 | 0 |  |  |  | 0 | $sth->execute() | 
| 1147 |  |  |  |  |  |  | or croak "Deletion failed: " . $dbh->errstr; | 
| 1148 |  |  |  |  |  |  |  | 
| 1149 |  |  |  |  |  |  | # All done | 
| 1150 | 0 |  |  |  |  | 0 | post_delete_node($name,$node_id,$version,$args{plugins}); | 
| 1151 | 0 |  |  |  |  | 0 | return 1; | 
| 1152 |  |  |  |  |  |  | } | 
| 1153 |  |  |  |  |  |  |  | 
| 1154 |  |  |  |  |  |  | # If we're still here, then we're deleting neither the latest | 
| 1155 |  |  |  |  |  |  | # nor the only version. | 
| 1156 | 0 |  |  |  |  | 0 | $sql = "DELETE FROM content | 
| 1157 |  |  |  |  |  |  | WHERE node_id = $node_id | 
| 1158 |  |  |  |  |  |  | AND version=?"; | 
| 1159 | 0 |  |  |  |  | 0 | $sth = $dbh->prepare( $sql ); | 
| 1160 | 0 | 0 |  |  |  | 0 | $sth->execute( $version ) | 
| 1161 |  |  |  |  |  |  | or croak "Deletion failed: " . $dbh->errstr; | 
| 1162 | 0 |  |  |  |  | 0 | $sql = "DELETE FROM metadata | 
| 1163 |  |  |  |  |  |  | WHERE node_id = $node_id | 
| 1164 |  |  |  |  |  |  | AND version=?"; | 
| 1165 | 0 |  |  |  |  | 0 | $sth = $dbh->prepare( $sql ); | 
| 1166 | 0 | 0 |  |  |  | 0 | $sth->execute( $version ) | 
| 1167 |  |  |  |  |  |  | or croak "Deletion failed: " . $dbh->errstr; | 
| 1168 |  |  |  |  |  |  |  | 
| 1169 |  |  |  |  |  |  | # All done | 
| 1170 | 0 |  |  |  |  | 0 | post_delete_node($name,$node_id,$version,$args{plugins}); | 
| 1171 | 0 |  |  |  |  | 0 | return 1; | 
| 1172 |  |  |  |  |  |  | } | 
| 1173 |  |  |  |  |  |  |  | 
| 1174 |  |  |  |  |  |  | # Returns the name of the node with the given ID | 
| 1175 |  |  |  |  |  |  | # Not normally used except when doing low-level maintenance | 
| 1176 |  |  |  |  |  |  | sub node_name_for_id { | 
| 1177 | 0 |  |  | 0 | 0 | 0 | my ($self, $node_id) = @_; | 
| 1178 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 1179 |  |  |  |  |  |  |  | 
| 1180 | 0 |  |  |  |  | 0 | my $name_sql = "SELECT name FROM node WHERE id=?"; | 
| 1181 | 0 |  |  |  |  | 0 | my $name_sth = $dbh->prepare($name_sql); | 
| 1182 | 0 |  |  |  |  | 0 | $name_sth->execute($node_id); | 
| 1183 | 0 |  |  |  |  | 0 | my ($name) = $name_sth->fetchrow_array; | 
| 1184 | 0 |  |  |  |  | 0 | $name_sth->finish; | 
| 1185 |  |  |  |  |  |  |  | 
| 1186 | 0 |  |  |  |  | 0 | return $name; | 
| 1187 |  |  |  |  |  |  | } | 
| 1188 |  |  |  |  |  |  |  | 
| 1189 |  |  |  |  |  |  | # Internal Method | 
| 1190 |  |  |  |  |  |  | sub post_delete_node { | 
| 1191 | 0 |  |  | 0 | 0 | 0 | my ($name,$node_id,$version,$plugins) = @_; | 
| 1192 |  |  |  |  |  |  |  | 
| 1193 |  |  |  |  |  |  | # Call post_delete on any plugins, having done the delete | 
| 1194 | 0 | 0 |  |  |  | 0 | my @plugins = @{ $plugins || [ ] }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 1195 | 0 |  |  |  |  | 0 | foreach my $plugin (@plugins) { | 
| 1196 | 0 | 0 |  |  |  | 0 | if ( $plugin->can( "post_delete" ) ) { | 
| 1197 | 0 |  |  |  |  | 0 | $plugin->post_delete( | 
| 1198 |  |  |  |  |  |  | node     => $name, | 
| 1199 |  |  |  |  |  |  | node_id  => $node_id, | 
| 1200 |  |  |  |  |  |  | version  => $version ); | 
| 1201 |  |  |  |  |  |  | } | 
| 1202 |  |  |  |  |  |  | } | 
| 1203 |  |  |  |  |  |  | } | 
| 1204 |  |  |  |  |  |  |  | 
| 1205 |  |  |  |  |  |  | =item B | 
| 1206 |  |  |  |  |  |  |  | 
| 1207 |  |  |  |  |  |  | # Nodes changed in last 7 days - each node listed only once. | 
| 1208 |  |  |  |  |  |  | my @nodes = $store->list_recent_changes( days => 7 ); | 
| 1209 |  |  |  |  |  |  |  | 
| 1210 |  |  |  |  |  |  | # Nodes added in the last 7 days. | 
| 1211 |  |  |  |  |  |  | my @nodes = $store->list_recent_changes( | 
| 1212 |  |  |  |  |  |  | days     => 7, | 
| 1213 |  |  |  |  |  |  | new_only => 1, | 
| 1214 |  |  |  |  |  |  | ); | 
| 1215 |  |  |  |  |  |  |  | 
| 1216 |  |  |  |  |  |  | # All changes in last 7 days - nodes changed more than once will | 
| 1217 |  |  |  |  |  |  | # be listed more than once. | 
| 1218 |  |  |  |  |  |  | my @nodes = $store->list_recent_changes( | 
| 1219 |  |  |  |  |  |  | days => 7, | 
| 1220 |  |  |  |  |  |  | include_all_changes => 1, | 
| 1221 |  |  |  |  |  |  | ); | 
| 1222 |  |  |  |  |  |  |  | 
| 1223 |  |  |  |  |  |  | # Nodes changed between 1 and 7 days ago. | 
| 1224 |  |  |  |  |  |  | my @nodes = $store->list_recent_changes( between_days => [ 1, 7 ] ); | 
| 1225 |  |  |  |  |  |  |  | 
| 1226 |  |  |  |  |  |  | # Nodes changed since a given time. | 
| 1227 |  |  |  |  |  |  | my @nodes = $store->list_recent_changes( since => 1036235131 ); | 
| 1228 |  |  |  |  |  |  |  | 
| 1229 |  |  |  |  |  |  | # Most recent change and its details. | 
| 1230 |  |  |  |  |  |  | my @nodes = $store->list_recent_changes( last_n_changes => 1 ); | 
| 1231 |  |  |  |  |  |  | print "Node:          $nodes[0]{name}"; | 
| 1232 |  |  |  |  |  |  | print "Last modified: $nodes[0]{last_modified}"; | 
| 1233 |  |  |  |  |  |  | print "Comment:       $nodes[0]{metadata}{comment}"; | 
| 1234 |  |  |  |  |  |  |  | 
| 1235 |  |  |  |  |  |  | # Last 5 restaurant nodes edited. | 
| 1236 |  |  |  |  |  |  | my @nodes = $store->list_recent_changes( | 
| 1237 |  |  |  |  |  |  | last_n_changes => 5, | 
| 1238 |  |  |  |  |  |  | metadata_is    => { category => "Restaurants" } | 
| 1239 |  |  |  |  |  |  | ); | 
| 1240 |  |  |  |  |  |  |  | 
| 1241 |  |  |  |  |  |  | # Last 5 nodes edited by Kake. | 
| 1242 |  |  |  |  |  |  | my @nodes = $store->list_recent_changes( | 
| 1243 |  |  |  |  |  |  | last_n_changes => 5, | 
| 1244 |  |  |  |  |  |  | metadata_was   => { username => "Kake" } | 
| 1245 |  |  |  |  |  |  | ); | 
| 1246 |  |  |  |  |  |  |  | 
| 1247 |  |  |  |  |  |  | # All minor edits made by Earle in the last week. | 
| 1248 |  |  |  |  |  |  | my @nodes = $store->list_recent_changes( | 
| 1249 |  |  |  |  |  |  | days           => 7, | 
| 1250 |  |  |  |  |  |  | metadata_was   => { username  => "Earle", | 
| 1251 |  |  |  |  |  |  | edit_type => "Minor tidying." } | 
| 1252 |  |  |  |  |  |  | ); | 
| 1253 |  |  |  |  |  |  |  | 
| 1254 |  |  |  |  |  |  | # Last 10 changes that weren't minor edits. | 
| 1255 |  |  |  |  |  |  | my @nodes = $store->list_recent_changes( | 
| 1256 |  |  |  |  |  |  | last_n_changes => 10, | 
| 1257 |  |  |  |  |  |  | metadata_wasnt  => { edit_type => "Minor tidying" } | 
| 1258 |  |  |  |  |  |  | ); | 
| 1259 |  |  |  |  |  |  |  | 
| 1260 |  |  |  |  |  |  | You I supply one of the following constraints: C | 
| 1261 |  |  |  |  |  |  | (integer), C (epoch), C (integer). | 
| 1262 |  |  |  |  |  |  |  | 
| 1263 |  |  |  |  |  |  | You I also supply moderation => 1 if you only want to see versions | 
| 1264 |  |  |  |  |  |  | that are moderated. | 
| 1265 |  |  |  |  |  |  |  | 
| 1266 |  |  |  |  |  |  | Another optional parameter is C, which if set to 1 will only | 
| 1267 |  |  |  |  |  |  | return newly added nodes. | 
| 1268 |  |  |  |  |  |  |  | 
| 1269 |  |  |  |  |  |  | You I also supply I C (and optionally | 
| 1270 |  |  |  |  |  |  | C), I C (and optionally | 
| 1271 |  |  |  |  |  |  | C). Each of these should be a ref to a hash with | 
| 1272 |  |  |  |  |  |  | scalar keys and values.  If the hash has more than one entry, then | 
| 1273 |  |  |  |  |  |  | only changes satisfying I criteria will be returned when using | 
| 1274 |  |  |  |  |  |  | C or C, but all changes which fail to | 
| 1275 |  |  |  |  |  |  | satisfy any one of the criteria will be returned when using | 
| 1276 |  |  |  |  |  |  | C or C. | 
| 1277 |  |  |  |  |  |  |  | 
| 1278 |  |  |  |  |  |  | C and C look only at the metadata that the | 
| 1279 |  |  |  |  |  |  | node I has. C and C take into | 
| 1280 |  |  |  |  |  |  | account the metadata of previous versions of a node.  Don't mix C | 
| 1281 |  |  |  |  |  |  | with C - there's no check for this, but the results are undefined. | 
| 1282 |  |  |  |  |  |  |  | 
| 1283 |  |  |  |  |  |  | Returns results as an array, in reverse chronological order.  Each | 
| 1284 |  |  |  |  |  |  | element of the array is a reference to a hash with the following entries: | 
| 1285 |  |  |  |  |  |  |  | 
| 1286 |  |  |  |  |  |  | =over 4 | 
| 1287 |  |  |  |  |  |  |  | 
| 1288 |  |  |  |  |  |  | =item * B: the name of the node | 
| 1289 |  |  |  |  |  |  |  | 
| 1290 |  |  |  |  |  |  | =item * B: the version number of the node | 
| 1291 |  |  |  |  |  |  |  | 
| 1292 |  |  |  |  |  |  | =item * B: timestamp showing when this version was written | 
| 1293 |  |  |  |  |  |  |  | 
| 1294 |  |  |  |  |  |  | =item * B: a ref to a hash containing any metadata attached | 
| 1295 |  |  |  |  |  |  | to this version of the node | 
| 1296 |  |  |  |  |  |  |  | 
| 1297 |  |  |  |  |  |  | =back | 
| 1298 |  |  |  |  |  |  |  | 
| 1299 |  |  |  |  |  |  | Unless you supply C, C or | 
| 1300 |  |  |  |  |  |  | C, each node will only be returned once regardless of | 
| 1301 |  |  |  |  |  |  | how many times it has been changed recently. | 
| 1302 |  |  |  |  |  |  |  | 
| 1303 |  |  |  |  |  |  | By default, the case-sensitivity of both C and | 
| 1304 |  |  |  |  |  |  | C depends on your database - if it will return rows | 
| 1305 |  |  |  |  |  |  | with an attribute value of "Pubs" when you asked for "pubs", or not. | 
| 1306 |  |  |  |  |  |  | If you supply a true value to the C parameter, then you | 
| 1307 |  |  |  |  |  |  | can be sure of its being case-insensitive.  This is recommended. | 
| 1308 |  |  |  |  |  |  |  | 
| 1309 |  |  |  |  |  |  | =cut | 
| 1310 |  |  |  |  |  |  |  | 
| 1311 |  |  |  |  |  |  | sub list_recent_changes { | 
| 1312 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 1313 | 0 |  |  |  |  | 0 | my %args = @_; | 
| 1314 | 0 | 0 |  |  |  | 0 | if ($args{since}) { | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
|  |  | 0 |  |  |  |  |  | 
| 1315 | 0 |  |  |  |  | 0 | return $self->_find_recent_changes_by_criteria( %args ); | 
| 1316 |  |  |  |  |  |  | } elsif ($args{between_days}) { | 
| 1317 | 0 |  |  |  |  | 0 | return $self->_find_recent_changes_by_criteria( %args ); | 
| 1318 |  |  |  |  |  |  | } elsif ( $args{days} ) { | 
| 1319 | 0 |  |  |  |  | 0 | my $now = localtime; | 
| 1320 | 0 |  |  |  |  | 0 | my $then = $now - ( ONE_DAY * $args{days} ); | 
| 1321 | 0 |  |  |  |  | 0 | $args{since} = $then; | 
| 1322 | 0 |  |  |  |  | 0 | delete $args{days}; | 
| 1323 | 0 |  |  |  |  | 0 | return $self->_find_recent_changes_by_criteria( %args ); | 
| 1324 |  |  |  |  |  |  | } elsif ( $args{last_n_changes} ) { | 
| 1325 | 0 |  |  |  |  | 0 | $args{limit} = delete $args{last_n_changes}; | 
| 1326 | 0 |  |  |  |  | 0 | return $self->_find_recent_changes_by_criteria( %args ); | 
| 1327 |  |  |  |  |  |  | } else { | 
| 1328 | 0 |  |  |  |  | 0 | croak "Need to supply some criteria to list_recent_changes."; | 
| 1329 |  |  |  |  |  |  | } | 
| 1330 |  |  |  |  |  |  | } | 
| 1331 |  |  |  |  |  |  |  | 
| 1332 |  |  |  |  |  |  | sub _find_recent_changes_by_criteria { | 
| 1333 | 0 |  |  | 0 |  | 0 | my ($self, %args) = @_; | 
| 1334 |  |  |  |  |  |  | my ($since, $limit, $between_days, $ignore_case, $new_only, | 
| 1335 |  |  |  |  |  |  | $metadata_is,  $metadata_isnt, $metadata_was, $metadata_wasnt, | 
| 1336 |  |  |  |  |  |  | $moderation, $include_all_changes ) = | 
| 1337 | 0 |  |  |  |  | 0 | @args{ qw( since limit between_days ignore_case new_only | 
| 1338 |  |  |  |  |  |  | metadata_is metadata_isnt metadata_was metadata_wasnt | 
| 1339 |  |  |  |  |  |  | moderation include_all_changes) }; | 
| 1340 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 1341 |  |  |  |  |  |  |  | 
| 1342 | 0 |  |  |  |  | 0 | my @where; | 
| 1343 |  |  |  |  |  |  | my @metadata_joins; | 
| 1344 | 0 |  |  |  |  | 0 | my $use_content_table; # some queries won't need this | 
| 1345 |  |  |  |  |  |  |  | 
| 1346 | 0 | 0 |  |  |  | 0 | if ( $metadata_is ) { | 
| 1347 | 0 |  |  |  |  | 0 | my $main_table = "node"; | 
| 1348 | 0 | 0 |  |  |  | 0 | if ( $include_all_changes ) { | 
| 1349 | 0 |  |  |  |  | 0 | $main_table = "content"; | 
| 1350 | 0 |  |  |  |  | 0 | $use_content_table = 1; | 
| 1351 |  |  |  |  |  |  | } | 
| 1352 | 0 |  |  |  |  | 0 | my $i = 0; | 
| 1353 | 0 |  |  |  |  | 0 | foreach my $type ( keys %$metadata_is ) { | 
| 1354 | 0 |  |  |  |  | 0 | $i++; | 
| 1355 | 0 |  |  |  |  | 0 | my $value  = $metadata_is->{$type}; | 
| 1356 | 0 | 0 |  |  |  | 0 | croak "metadata_is must have scalar values" if ref $value; | 
| 1357 | 0 |  |  |  |  | 0 | my $mdt = "md_is_$i"; | 
| 1358 | 0 | 0 |  |  |  | 0 | push @metadata_joins, "LEFT JOIN metadata AS $mdt | 
| 1359 |  |  |  |  |  |  | ON $main_table." | 
| 1360 |  |  |  |  |  |  | . ( ($main_table eq "node") ? "id" | 
| 1361 |  |  |  |  |  |  | : "node_id" ) | 
| 1362 |  |  |  |  |  |  | . "=$mdt.node_id | 
| 1363 |  |  |  |  |  |  | AND $main_table.version=$mdt.version\n"; | 
| 1364 |  |  |  |  |  |  | # Why is this inside 'if ( $metadata_is )'? | 
| 1365 |  |  |  |  |  |  | # Shouldn't it apply to all cases? | 
| 1366 |  |  |  |  |  |  | # What's it doing in @metadata_joins? | 
| 1367 | 0 | 0 |  |  |  | 0 | if (defined $moderation) { | 
| 1368 | 0 |  |  |  |  | 0 | push @metadata_joins, "AND $main_table.moderate=$moderation"; | 
| 1369 |  |  |  |  |  |  | } | 
| 1370 | 0 |  |  |  |  | 0 | push @where, "( " | 
| 1371 |  |  |  |  |  |  | . $self->_get_comparison_sql( | 
| 1372 |  |  |  |  |  |  | thing1      => "$mdt.metadata_type", | 
| 1373 |  |  |  |  |  |  | thing2      => $dbh->quote($type), | 
| 1374 |  |  |  |  |  |  | ignore_case => $ignore_case, | 
| 1375 |  |  |  |  |  |  | ) | 
| 1376 |  |  |  |  |  |  | . " AND " | 
| 1377 |  |  |  |  |  |  | . $self->_get_comparison_sql( | 
| 1378 |  |  |  |  |  |  | thing1      => "$mdt.metadata_value", | 
| 1379 |  |  |  |  |  |  | thing2      => $dbh->quote( $self->charset_encode($value) ), | 
| 1380 |  |  |  |  |  |  | Ignore_case => $ignore_case, | 
| 1381 |  |  |  |  |  |  | ) | 
| 1382 |  |  |  |  |  |  | . " )"; | 
| 1383 |  |  |  |  |  |  | } | 
| 1384 |  |  |  |  |  |  | } | 
| 1385 |  |  |  |  |  |  |  | 
| 1386 | 0 | 0 |  |  |  | 0 | if ( $metadata_isnt ) { | 
| 1387 | 0 |  |  |  |  | 0 | foreach my $type ( keys %$metadata_isnt ) { | 
| 1388 | 0 |  |  |  |  | 0 | my $value  = $metadata_isnt->{$type}; | 
| 1389 | 0 | 0 |  |  |  | 0 | croak "metadata_isnt must have scalar values" if ref $value; | 
| 1390 |  |  |  |  |  |  | } | 
| 1391 | 0 |  |  |  |  | 0 | my @omits = $self->_find_recent_changes_by_criteria( | 
| 1392 |  |  |  |  |  |  | since        => $since, | 
| 1393 |  |  |  |  |  |  | between_days => $between_days, | 
| 1394 |  |  |  |  |  |  | metadata_is  => $metadata_isnt, | 
| 1395 |  |  |  |  |  |  | ignore_case  => $ignore_case, | 
| 1396 |  |  |  |  |  |  | ); | 
| 1397 | 0 |  |  |  |  | 0 | foreach my $omit ( @omits ) { | 
| 1398 |  |  |  |  |  |  | push @where, "( node.name != " . $dbh->quote($omit->{name}) | 
| 1399 |  |  |  |  |  |  | . "  OR node.version != " . $dbh->quote($omit->{version}) | 
| 1400 | 0 |  |  |  |  | 0 | . ")"; | 
| 1401 |  |  |  |  |  |  | } | 
| 1402 |  |  |  |  |  |  | } | 
| 1403 |  |  |  |  |  |  |  | 
| 1404 | 0 | 0 |  |  |  | 0 | if ( $metadata_was ) { | 
| 1405 | 0 |  |  |  |  | 0 | $use_content_table = 1; | 
| 1406 | 0 |  |  |  |  | 0 | my $i = 0; | 
| 1407 | 0 |  |  |  |  | 0 | foreach my $type ( keys %$metadata_was ) { | 
| 1408 | 0 |  |  |  |  | 0 | $i++; | 
| 1409 | 0 |  |  |  |  | 0 | my $value  = $metadata_was->{$type}; | 
| 1410 | 0 | 0 |  |  |  | 0 | croak "metadata_was must have scalar values" if ref $value; | 
| 1411 | 0 |  |  |  |  | 0 | my $mdt = "md_was_$i"; | 
| 1412 | 0 |  |  |  |  | 0 | push @metadata_joins, "LEFT JOIN metadata AS $mdt | 
| 1413 |  |  |  |  |  |  | ON content.node_id=$mdt.node_id | 
| 1414 |  |  |  |  |  |  | AND content.version=$mdt.version\n"; | 
| 1415 | 0 |  |  |  |  | 0 | push @where, "( " | 
| 1416 |  |  |  |  |  |  | . $self->_get_comparison_sql( | 
| 1417 |  |  |  |  |  |  | thing1      => "$mdt.metadata_type", | 
| 1418 |  |  |  |  |  |  | thing2      => $dbh->quote($type), | 
| 1419 |  |  |  |  |  |  | ignore_case => $ignore_case, | 
| 1420 |  |  |  |  |  |  | ) | 
| 1421 |  |  |  |  |  |  | . " AND " | 
| 1422 |  |  |  |  |  |  | . $self->_get_comparison_sql( | 
| 1423 |  |  |  |  |  |  | thing1      => "$mdt.metadata_value", | 
| 1424 |  |  |  |  |  |  | thing2      => $dbh->quote( $self->charset_encode($value) ), | 
| 1425 |  |  |  |  |  |  | ignore_case => $ignore_case, | 
| 1426 |  |  |  |  |  |  | ) | 
| 1427 |  |  |  |  |  |  | . " )"; | 
| 1428 |  |  |  |  |  |  | } | 
| 1429 |  |  |  |  |  |  | } | 
| 1430 |  |  |  |  |  |  |  | 
| 1431 | 0 | 0 |  |  |  | 0 | if ( $metadata_wasnt ) { | 
| 1432 | 0 |  |  |  |  | 0 | foreach my $type ( keys %$metadata_wasnt ) { | 
| 1433 | 0 |  |  |  |  | 0 | my $value  = $metadata_was->{$type}; | 
| 1434 | 0 | 0 |  |  |  | 0 | croak "metadata_was must have scalar values" if ref $value; | 
| 1435 |  |  |  |  |  |  | } | 
| 1436 | 0 |  |  |  |  | 0 | my @omits = $self->_find_recent_changes_by_criteria( | 
| 1437 |  |  |  |  |  |  | since        => $since, | 
| 1438 |  |  |  |  |  |  | between_days => $between_days, | 
| 1439 |  |  |  |  |  |  | metadata_was => $metadata_wasnt, | 
| 1440 |  |  |  |  |  |  | ignore_case  => $ignore_case, | 
| 1441 |  |  |  |  |  |  | ); | 
| 1442 | 0 |  |  |  |  | 0 | foreach my $omit ( @omits ) { | 
| 1443 |  |  |  |  |  |  | push @where, "( node.name != " . $dbh->quote($omit->{name}) | 
| 1444 |  |  |  |  |  |  | . "  OR content.version != " . $dbh->quote($omit->{version}) | 
| 1445 | 0 |  |  |  |  | 0 | . ")"; | 
| 1446 |  |  |  |  |  |  | } | 
| 1447 | 0 |  |  |  |  | 0 | $use_content_table = 1; | 
| 1448 |  |  |  |  |  |  | } | 
| 1449 |  |  |  |  |  |  |  | 
| 1450 |  |  |  |  |  |  | # Figure out which table we should be joining to to check the dates and | 
| 1451 |  |  |  |  |  |  | # versions - node or content. | 
| 1452 | 0 |  |  |  |  | 0 | my $date_table = "node"; | 
| 1453 | 0 | 0 | 0 |  |  | 0 | if ( $include_all_changes || $new_only | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
| 1454 |  |  |  |  |  |  | || $metadata_was || $metadata_wasnt ) { | 
| 1455 | 0 |  |  |  |  | 0 | $date_table = "content"; | 
| 1456 | 0 |  |  |  |  | 0 | $use_content_table = 1; | 
| 1457 |  |  |  |  |  |  | } | 
| 1458 | 0 | 0 |  |  |  | 0 | if ( $new_only ) { | 
| 1459 | 0 |  |  |  |  | 0 | push @where, "content.version=1"; | 
| 1460 |  |  |  |  |  |  | } | 
| 1461 |  |  |  |  |  |  |  | 
| 1462 | 0 | 0 |  |  |  | 0 | if ( $since ) { | 
|  |  | 0 |  |  |  |  |  | 
| 1463 | 0 |  |  |  |  | 0 | my $timestamp = $self->_get_timestamp( $since ); | 
| 1464 | 0 |  |  |  |  | 0 | push @where, "$date_table.modified >= " . $dbh->quote($timestamp); | 
| 1465 |  |  |  |  |  |  | } elsif ( $between_days ) { | 
| 1466 | 0 |  |  |  |  | 0 | my $now = localtime; | 
| 1467 |  |  |  |  |  |  | # Start is the larger number of days ago. | 
| 1468 | 0 |  |  |  |  | 0 | my ($start, $end) = @$between_days; | 
| 1469 | 0 | 0 |  |  |  | 0 | ($start, $end) = ($end, $start) if $start < $end; | 
| 1470 | 0 |  |  |  |  | 0 | my $ts_start = $self->_get_timestamp( $now - (ONE_DAY * $start) ); | 
| 1471 | 0 |  |  |  |  | 0 | my $ts_end = $self->_get_timestamp( $now - (ONE_DAY * $end) ); | 
| 1472 | 0 |  |  |  |  | 0 | push @where, "$date_table.modified >= " . $dbh->quote($ts_start); | 
| 1473 | 0 |  |  |  |  | 0 | push @where, "$date_table.modified <= " . $dbh->quote($ts_end); | 
| 1474 |  |  |  |  |  |  | } | 
| 1475 |  |  |  |  |  |  |  | 
| 1476 | 0 |  |  |  |  | 0 | my $sql = "SELECT DISTINCT | 
| 1477 |  |  |  |  |  |  | node.name, | 
| 1478 |  |  |  |  |  |  | "; | 
| 1479 | 0 | 0 | 0 |  |  | 0 | if ( $include_all_changes || $new_only || $use_content_table ) { | 
|  |  |  | 0 |  |  |  |  | 
| 1480 | 0 |  |  |  |  | 0 | $sql .= " content.version, content.modified "; | 
| 1481 |  |  |  |  |  |  | } else { | 
| 1482 | 0 |  |  |  |  | 0 | $sql .= " node.version, node.modified "; | 
| 1483 |  |  |  |  |  |  | } | 
| 1484 | 0 |  |  |  |  | 0 | $sql .= " FROM node "; | 
| 1485 | 0 | 0 |  |  |  | 0 | if ( $use_content_table ) { | 
| 1486 | 0 |  |  |  |  | 0 | $sql .= " INNER JOIN content ON (node.id = content.node_id ) "; | 
| 1487 |  |  |  |  |  |  | } | 
| 1488 |  |  |  |  |  |  |  | 
| 1489 | 0 | 0 |  |  |  | 0 | $sql .= join("\n", @metadata_joins) | 
|  |  | 0 |  |  |  |  |  | 
| 1490 |  |  |  |  |  |  | . ( | 
| 1491 |  |  |  |  |  |  | scalar @where | 
| 1492 |  |  |  |  |  |  | ? " WHERE " . join(" AND ",@where) | 
| 1493 |  |  |  |  |  |  | : "" | 
| 1494 |  |  |  |  |  |  | ) | 
| 1495 |  |  |  |  |  |  | . " ORDER BY " | 
| 1496 |  |  |  |  |  |  | . ( $use_content_table ? "content" : "node" ) | 
| 1497 |  |  |  |  |  |  | . ".modified DESC"; | 
| 1498 | 0 | 0 |  |  |  | 0 | if ( $limit ) { | 
| 1499 | 0 | 0 |  |  |  | 0 | croak "Bad argument $limit" unless $limit =~ /^\d+$/; | 
| 1500 | 0 |  |  |  |  | 0 | $sql .= " LIMIT $limit"; | 
| 1501 |  |  |  |  |  |  | } | 
| 1502 | 0 |  |  |  |  | 0 | my $nodesref = $dbh->selectall_arrayref($sql); | 
| 1503 | 0 |  |  |  |  | 0 | my @finds = map { { name          => $_->[0], | 
|  | 0 |  |  |  |  | 0 |  | 
| 1504 |  |  |  |  |  |  | version       => $_->[1], | 
| 1505 |  |  |  |  |  |  | last_modified => $_->[2] } | 
| 1506 |  |  |  |  |  |  | } @$nodesref; | 
| 1507 | 0 |  |  |  |  | 0 | foreach my $find ( @finds ) { | 
| 1508 | 0 |  |  |  |  | 0 | my %metadata; | 
| 1509 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare( "SELECT metadata_type, metadata_value | 
| 1510 |  |  |  |  |  |  | FROM node | 
| 1511 |  |  |  |  |  |  | INNER JOIN metadata | 
| 1512 |  |  |  |  |  |  | ON (id = node_id) | 
| 1513 |  |  |  |  |  |  | WHERE name=? | 
| 1514 |  |  |  |  |  |  | AND metadata.version=?" ); | 
| 1515 | 0 |  |  |  |  | 0 | $sth->execute( $find->{name}, $find->{version} ); | 
| 1516 | 0 |  |  |  |  | 0 | while ( my ($type, $value) = $self->charset_decode( $sth->fetchrow_array ) ) { | 
| 1517 | 0 | 0 |  |  |  | 0 | if ( defined $metadata{$type} ) { | 
| 1518 | 0 |  |  |  |  | 0 | push @{$metadata{$type}}, $value; | 
|  | 0 |  |  |  |  | 0 |  | 
| 1519 |  |  |  |  |  |  | } else { | 
| 1520 | 0 |  |  |  |  | 0 | $metadata{$type} = [ $value ]; | 
| 1521 |  |  |  |  |  |  | } | 
| 1522 |  |  |  |  |  |  | } | 
| 1523 | 0 |  |  |  |  | 0 | $find->{metadata} = \%metadata; | 
| 1524 |  |  |  |  |  |  | } | 
| 1525 | 0 |  |  |  |  | 0 | return @finds; | 
| 1526 |  |  |  |  |  |  | } | 
| 1527 |  |  |  |  |  |  |  | 
| 1528 |  |  |  |  |  |  | =item B | 
| 1529 |  |  |  |  |  |  |  | 
| 1530 |  |  |  |  |  |  | my @nodes = $store->list_all_nodes(); | 
| 1531 |  |  |  |  |  |  | print "First node is $nodes[0]\n"; | 
| 1532 |  |  |  |  |  |  |  | 
| 1533 |  |  |  |  |  |  | my @nodes = $store->list_all_nodes( with_details=> 1 ); | 
| 1534 |  |  |  |  |  |  | print "First node is ".$nodes[0]->{'name'}." at version ".$nodes[0]->{'version'}."\n"; | 
| 1535 |  |  |  |  |  |  |  | 
| 1536 |  |  |  |  |  |  | Returns a list containing the name of every existing node.  The list | 
| 1537 |  |  |  |  |  |  | won't be in any kind of order; do any sorting in your calling script. | 
| 1538 |  |  |  |  |  |  |  | 
| 1539 |  |  |  |  |  |  | Optionally also returns the id, version and moderation flag. | 
| 1540 |  |  |  |  |  |  |  | 
| 1541 |  |  |  |  |  |  | =cut | 
| 1542 |  |  |  |  |  |  |  | 
| 1543 |  |  |  |  |  |  | sub list_all_nodes { | 
| 1544 | 0 |  |  | 0 | 1 | 0 | my ($self,%args) = @_; | 
| 1545 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 1546 | 0 |  |  |  |  | 0 | my @nodes; | 
| 1547 |  |  |  |  |  |  |  | 
| 1548 | 0 | 0 |  |  |  | 0 | if($args{with_details}) { | 
| 1549 | 0 |  |  |  |  | 0 | my $sql = "SELECT id, name, version, moderate FROM node;"; | 
| 1550 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare( $sql ); | 
| 1551 | 0 |  |  |  |  | 0 | $sth->execute(); | 
| 1552 |  |  |  |  |  |  |  | 
| 1553 | 0 |  |  |  |  | 0 | while(my @results = $sth->fetchrow_array) { | 
| 1554 | 0 |  |  |  |  | 0 | my %data; | 
| 1555 | 0 |  |  |  |  | 0 | @data{ qw( node_id name version moderate ) } = @results; | 
| 1556 | 0 |  |  |  |  | 0 | push @nodes, \%data; | 
| 1557 |  |  |  |  |  |  | } | 
| 1558 |  |  |  |  |  |  | } else { | 
| 1559 | 0 |  |  |  |  | 0 | my $sql = "SELECT name FROM node;"; | 
| 1560 | 0 |  |  |  |  | 0 | my $raw_nodes = $dbh->selectall_arrayref($sql); | 
| 1561 | 0 |  |  |  |  | 0 | @nodes = ( map { $self->charset_decode( $_->[0] ) } (@$raw_nodes) ); | 
|  | 0 |  |  |  |  | 0 |  | 
| 1562 |  |  |  |  |  |  | } | 
| 1563 | 0 |  |  |  |  | 0 | return @nodes; | 
| 1564 |  |  |  |  |  |  | } | 
| 1565 |  |  |  |  |  |  |  | 
| 1566 |  |  |  |  |  |  | =item B | 
| 1567 |  |  |  |  |  |  |  | 
| 1568 |  |  |  |  |  |  | my @all_versions = $store->list_node_all_versions( | 
| 1569 |  |  |  |  |  |  | name => 'HomePage', | 
| 1570 |  |  |  |  |  |  | with_content => 1, | 
| 1571 |  |  |  |  |  |  | with_metadata => 0 | 
| 1572 |  |  |  |  |  |  | ); | 
| 1573 |  |  |  |  |  |  |  | 
| 1574 |  |  |  |  |  |  | Returns all the versions of a node, optionally including the content | 
| 1575 |  |  |  |  |  |  | and metadata, as an array of hashes (newest versions first). | 
| 1576 |  |  |  |  |  |  |  | 
| 1577 |  |  |  |  |  |  | =cut | 
| 1578 |  |  |  |  |  |  |  | 
| 1579 |  |  |  |  |  |  | sub list_node_all_versions { | 
| 1580 | 0 |  |  | 0 | 1 | 0 | my ($self, %args) = @_; | 
| 1581 |  |  |  |  |  |  |  | 
| 1582 |  |  |  |  |  |  | my ($node_id,$name,$with_content,$with_metadata) = | 
| 1583 | 0 |  |  |  |  | 0 | @args{ qw( node_id name with_content with_metadata ) }; | 
| 1584 |  |  |  |  |  |  |  | 
| 1585 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 1586 | 0 |  |  |  |  | 0 | my $sql; | 
| 1587 |  |  |  |  |  |  |  | 
| 1588 |  |  |  |  |  |  | # If they only gave us the node name, get the node id | 
| 1589 | 0 | 0 |  |  |  | 0 | unless ($node_id) { | 
| 1590 | 0 |  |  |  |  | 0 | $sql = "SELECT id FROM node WHERE name=" . $dbh->quote($name); | 
| 1591 | 0 |  |  |  |  | 0 | $node_id = $dbh->selectrow_array($sql); | 
| 1592 |  |  |  |  |  |  | } | 
| 1593 |  |  |  |  |  |  |  | 
| 1594 |  |  |  |  |  |  | # If they didn't tell us what they wanted / we couldn't find it, | 
| 1595 |  |  |  |  |  |  | #  return an empty array | 
| 1596 | 0 | 0 |  |  |  | 0 | return () unless($node_id); | 
| 1597 |  |  |  |  |  |  |  | 
| 1598 |  |  |  |  |  |  | # Build up our SQL | 
| 1599 | 0 |  |  |  |  | 0 | $sql = "SELECT id, name, content.version, content.modified "; | 
| 1600 | 0 | 0 |  |  |  | 0 | if ( $with_content ) { | 
| 1601 | 0 |  |  |  |  | 0 | $sql .= ", content.text "; | 
| 1602 |  |  |  |  |  |  | } | 
| 1603 | 0 | 0 |  |  |  | 0 | if ( $with_metadata ) { | 
| 1604 | 0 |  |  |  |  | 0 | $sql .= ", metadata_type, metadata_value "; | 
| 1605 |  |  |  |  |  |  | } | 
| 1606 | 0 |  |  |  |  | 0 | $sql .= " FROM node INNER JOIN content ON (id = content.node_id) "; | 
| 1607 | 0 | 0 |  |  |  | 0 | if ( $with_metadata ) { | 
| 1608 | 0 |  |  |  |  | 0 | $sql .= " LEFT OUTER JOIN metadata ON " | 
| 1609 |  |  |  |  |  |  | . "(id = metadata.node_id AND content.version = metadata.version) "; | 
| 1610 |  |  |  |  |  |  | } | 
| 1611 | 0 |  |  |  |  | 0 | $sql .= " WHERE id = ? ORDER BY content.version DESC"; | 
| 1612 |  |  |  |  |  |  |  | 
| 1613 |  |  |  |  |  |  | # Do the fetch | 
| 1614 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare( $sql ); | 
| 1615 | 0 |  |  |  |  | 0 | $sth->execute( $node_id ); | 
| 1616 |  |  |  |  |  |  |  | 
| 1617 |  |  |  |  |  |  | # Need to hold onto the last row by hash ref, so we don't trash | 
| 1618 |  |  |  |  |  |  | #  it every time | 
| 1619 | 0 |  |  |  |  | 0 | my %first_data; | 
| 1620 | 0 |  |  |  |  | 0 | my $dataref = \%first_data; | 
| 1621 |  |  |  |  |  |  |  | 
| 1622 |  |  |  |  |  |  | # Haul out the data | 
| 1623 | 0 |  |  |  |  | 0 | my @versions; | 
| 1624 | 0 |  |  |  |  | 0 | while ( my @results = $sth->fetchrow_array ) { | 
| 1625 | 0 |  |  |  |  | 0 | my %data = %$dataref; | 
| 1626 |  |  |  |  |  |  |  | 
| 1627 |  |  |  |  |  |  | # Is it the same version as last time? | 
| 1628 | 0 | 0 | 0 |  |  | 0 | if ( %data && $data{'version'} != $results[2] ) { | 
| 1629 |  |  |  |  |  |  | # New version | 
| 1630 | 0 |  |  |  |  | 0 | push @versions, $dataref; | 
| 1631 | 0 |  |  |  |  | 0 | %data = (); | 
| 1632 |  |  |  |  |  |  | } else { | 
| 1633 |  |  |  |  |  |  | # Same version as last time, must be more metadata | 
| 1634 |  |  |  |  |  |  | } | 
| 1635 |  |  |  |  |  |  |  | 
| 1636 |  |  |  |  |  |  | # Grab the core data (will be the same on multi-row for metadata) | 
| 1637 | 0 |  |  |  |  | 0 | @data{ qw( node_id name version last_modified ) } = @results; | 
| 1638 |  |  |  |  |  |  |  | 
| 1639 | 0 |  |  |  |  | 0 | my $i = 4; | 
| 1640 | 0 | 0 |  |  |  | 0 | if ( $with_content ) { | 
| 1641 | 0 |  |  |  |  | 0 | $data{'content'} = $results[$i]; | 
| 1642 | 0 |  |  |  |  | 0 | $i++; | 
| 1643 |  |  |  |  |  |  | } | 
| 1644 | 0 | 0 |  |  |  | 0 | if ( $with_metadata ) { | 
| 1645 | 0 |  |  |  |  | 0 | my ($m_type,$m_value) = @results[$i,($i+1)]; | 
| 1646 | 0 | 0 |  |  |  | 0 | unless ( $data{'metadata'} ) { $data{'metadata'} = {}; } | 
|  | 0 |  |  |  |  | 0 |  | 
| 1647 |  |  |  |  |  |  |  | 
| 1648 | 0 | 0 |  |  |  | 0 | if ( $m_type ) { | 
| 1649 |  |  |  |  |  |  | # If we have existing data, then put it into an array | 
| 1650 | 0 | 0 |  |  |  | 0 | if ( $data{'metadata'}->{$m_type} ) { | 
| 1651 | 0 | 0 |  |  |  | 0 | unless ( ref($data{'metadata'}->{$m_type}) eq "ARRAY" ) { | 
| 1652 |  |  |  |  |  |  | $data{'metadata'}->{$m_type} = | 
| 1653 | 0 |  |  |  |  | 0 | [ $data{'metadata'}->{$m_type} ]; | 
| 1654 |  |  |  |  |  |  | } | 
| 1655 | 0 |  |  |  |  | 0 | push @{$data{'metadata'}->{$m_type}}, $m_value; | 
|  | 0 |  |  |  |  | 0 |  | 
| 1656 |  |  |  |  |  |  | } else { | 
| 1657 |  |  |  |  |  |  | # Otherwise, just store it in a normal string | 
| 1658 | 0 |  |  |  |  | 0 | $data{'metadata'}->{$m_type} = $m_value; | 
| 1659 |  |  |  |  |  |  | } | 
| 1660 |  |  |  |  |  |  | } | 
| 1661 |  |  |  |  |  |  | } | 
| 1662 |  |  |  |  |  |  |  | 
| 1663 |  |  |  |  |  |  | # Save where we've got to | 
| 1664 | 0 |  |  |  |  | 0 | $dataref = \%data; | 
| 1665 |  |  |  |  |  |  | } | 
| 1666 |  |  |  |  |  |  |  | 
| 1667 |  |  |  |  |  |  | # Handle final row saving | 
| 1668 | 0 | 0 |  |  |  | 0 | if ( $dataref ) { | 
| 1669 | 0 |  |  |  |  | 0 | push @versions, $dataref; | 
| 1670 |  |  |  |  |  |  | } | 
| 1671 |  |  |  |  |  |  |  | 
| 1672 |  |  |  |  |  |  | # Return | 
| 1673 | 0 |  |  |  |  | 0 | return @versions; | 
| 1674 |  |  |  |  |  |  | } | 
| 1675 |  |  |  |  |  |  |  | 
| 1676 |  |  |  |  |  |  | =item B | 
| 1677 |  |  |  |  |  |  |  | 
| 1678 |  |  |  |  |  |  | # All documentation nodes. | 
| 1679 |  |  |  |  |  |  | my @nodes = $store->list_nodes_by_metadata( | 
| 1680 |  |  |  |  |  |  | metadata_type  => "category", | 
| 1681 |  |  |  |  |  |  | metadata_value => "documentation", | 
| 1682 |  |  |  |  |  |  | ignore_case    => 1,   # optional but recommended (see below) | 
| 1683 |  |  |  |  |  |  | ); | 
| 1684 |  |  |  |  |  |  |  | 
| 1685 |  |  |  |  |  |  | # All pubs in Hammersmith. | 
| 1686 |  |  |  |  |  |  | my @pubs = $store->list_nodes_by_metadata( | 
| 1687 |  |  |  |  |  |  | metadata_type  => "category", | 
| 1688 |  |  |  |  |  |  | metadata_value => "Pub", | 
| 1689 |  |  |  |  |  |  | ); | 
| 1690 |  |  |  |  |  |  | my @hsm  = $store->list_nodes_by_metadata( | 
| 1691 |  |  |  |  |  |  | metadata_type  => "category", | 
| 1692 |  |  |  |  |  |  | metadata_value  => "Hammersmith", | 
| 1693 |  |  |  |  |  |  | ); | 
| 1694 |  |  |  |  |  |  | my @results = my_l33t_method_for_ANDing_arrays( \@pubs, \@hsm ); | 
| 1695 |  |  |  |  |  |  |  | 
| 1696 |  |  |  |  |  |  | Returns a list containing the name of every node whose caller-supplied | 
| 1697 |  |  |  |  |  |  | metadata matches the criteria given in the parameters. | 
| 1698 |  |  |  |  |  |  |  | 
| 1699 |  |  |  |  |  |  | By default, the case-sensitivity of both C and | 
| 1700 |  |  |  |  |  |  | C depends on your database - if it will return rows | 
| 1701 |  |  |  |  |  |  | with an attribute value of "Pubs" when you asked for "pubs", or not. | 
| 1702 |  |  |  |  |  |  | If you supply a true value to the C parameter, then you | 
| 1703 |  |  |  |  |  |  | can be sure of its being case-insensitive.  This is recommended. | 
| 1704 |  |  |  |  |  |  |  | 
| 1705 |  |  |  |  |  |  | If you don't supply any criteria then you'll get an empty list. | 
| 1706 |  |  |  |  |  |  |  | 
| 1707 |  |  |  |  |  |  | This is a really really really simple way of finding things; if you | 
| 1708 |  |  |  |  |  |  | want to be more complicated then you'll need to call the method | 
| 1709 |  |  |  |  |  |  | multiple times and combine the results yourself, or write a plugin. | 
| 1710 |  |  |  |  |  |  |  | 
| 1711 |  |  |  |  |  |  | =cut | 
| 1712 |  |  |  |  |  |  |  | 
| 1713 |  |  |  |  |  |  | sub list_nodes_by_metadata { | 
| 1714 | 0 |  |  | 0 | 1 | 0 | my ($self, %args) = @_; | 
| 1715 | 0 |  |  |  |  | 0 | my ( $type, $value ) = @args{ qw( metadata_type metadata_value ) }; | 
| 1716 | 0 | 0 |  |  |  | 0 | return () unless $type; | 
| 1717 |  |  |  |  |  |  |  | 
| 1718 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 1719 | 0 | 0 |  |  |  | 0 | if ( $args{ignore_case} ) { | 
| 1720 | 0 |  |  |  |  | 0 | $type  = lc( $type  ); | 
| 1721 | 0 |  |  |  |  | 0 | $value = lc( $value ); | 
| 1722 |  |  |  |  |  |  | } | 
| 1723 |  |  |  |  |  |  | my $sql = | 
| 1724 | 0 |  |  |  |  | 0 | $self->_get_list_by_metadata_sql( ignore_case => $args{ignore_case} ); | 
| 1725 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare( $sql ); | 
| 1726 | 0 |  |  |  |  | 0 | $sth->execute( $type, $self->charset_encode($value) ); | 
| 1727 | 0 |  |  |  |  | 0 | my @nodes; | 
| 1728 | 0 |  |  |  |  | 0 | while ( my ($id, $node) = $sth->fetchrow_array ) { | 
| 1729 | 0 |  |  |  |  | 0 | push @nodes, $node; | 
| 1730 |  |  |  |  |  |  | } | 
| 1731 | 0 |  |  |  |  | 0 | return @nodes; | 
| 1732 |  |  |  |  |  |  | } | 
| 1733 |  |  |  |  |  |  |  | 
| 1734 |  |  |  |  |  |  | =item B | 
| 1735 |  |  |  |  |  |  | Returns nodes where either the metadata doesn't exist, or is blank | 
| 1736 |  |  |  |  |  |  |  | 
| 1737 |  |  |  |  |  |  | Unlike list_nodes_by_metadata(), the metadata value is optional. | 
| 1738 |  |  |  |  |  |  |  | 
| 1739 |  |  |  |  |  |  | # All nodes missing documentation | 
| 1740 |  |  |  |  |  |  | my @nodes = $store->list_nodes_by_missing_metadata( | 
| 1741 |  |  |  |  |  |  | metadata_type  => "category", | 
| 1742 |  |  |  |  |  |  | metadata_value => "documentation", | 
| 1743 |  |  |  |  |  |  | ignore_case    => 1,   # optional but recommended (see below) | 
| 1744 |  |  |  |  |  |  | ); | 
| 1745 |  |  |  |  |  |  |  | 
| 1746 |  |  |  |  |  |  | # All nodes which don't have a latitude defined | 
| 1747 |  |  |  |  |  |  | my @nodes = $store->list_nodes_by_missing_metadata( | 
| 1748 |  |  |  |  |  |  | metadata_type  => "latitude" | 
| 1749 |  |  |  |  |  |  | ); | 
| 1750 |  |  |  |  |  |  |  | 
| 1751 |  |  |  |  |  |  | =cut | 
| 1752 |  |  |  |  |  |  |  | 
| 1753 |  |  |  |  |  |  | sub list_nodes_by_missing_metadata { | 
| 1754 | 0 |  |  | 0 | 1 | 0 | my ($self, %args) = @_; | 
| 1755 | 0 |  |  |  |  | 0 | my ( $type, $value ) = @args{ qw( metadata_type metadata_value ) }; | 
| 1756 | 0 | 0 |  |  |  | 0 | return () unless $type; | 
| 1757 |  |  |  |  |  |  |  | 
| 1758 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 1759 | 0 | 0 |  |  |  | 0 | if ( $args{ignore_case} ) { | 
| 1760 | 0 |  |  |  |  | 0 | $type  = lc( $type  ); | 
| 1761 | 0 |  |  |  |  | 0 | $value = lc( $value ); | 
| 1762 |  |  |  |  |  |  | } | 
| 1763 |  |  |  |  |  |  |  | 
| 1764 | 0 |  |  |  |  | 0 | my @nodes; | 
| 1765 |  |  |  |  |  |  |  | 
| 1766 |  |  |  |  |  |  | # If the don't want to match by value, then we can do it with | 
| 1767 |  |  |  |  |  |  | #  a LEFT OUTER JOIN, and either NULL or LENGTH() = 0 | 
| 1768 | 0 | 0 |  |  |  | 0 | if( ! $value ) { | 
| 1769 |  |  |  |  |  |  | my $sql = $self->_get_list_by_missing_metadata_sql( | 
| 1770 |  |  |  |  |  |  | ignore_case => $args{ignore_case} | 
| 1771 | 0 |  |  |  |  | 0 | ); | 
| 1772 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare( $sql ); | 
| 1773 | 0 |  |  |  |  | 0 | $sth->execute( $type ); | 
| 1774 |  |  |  |  |  |  |  | 
| 1775 | 0 |  |  |  |  | 0 | while ( my ($id, $node) = $sth->fetchrow_array ) { | 
| 1776 | 0 |  |  |  |  | 0 | push @nodes, $node; | 
| 1777 |  |  |  |  |  |  | } | 
| 1778 |  |  |  |  |  |  | } else { | 
| 1779 |  |  |  |  |  |  | # To find those without the value in this case would involve | 
| 1780 |  |  |  |  |  |  | #  some seriously brain hurting SQL. | 
| 1781 |  |  |  |  |  |  | # So, cheat - find those with, and return everything else | 
| 1782 | 0 |  |  |  |  | 0 | my @with = $self->list_nodes_by_metadata(%args); | 
| 1783 | 0 |  |  |  |  | 0 | my %with_hash; | 
| 1784 | 0 |  |  |  |  | 0 | foreach my $node (@with) { $with_hash{$node} = 1; } | 
|  | 0 |  |  |  |  | 0 |  | 
| 1785 |  |  |  |  |  |  |  | 
| 1786 | 0 |  |  |  |  | 0 | my @all_nodes = $self->list_all_nodes(); | 
| 1787 | 0 |  |  |  |  | 0 | foreach my $node (@all_nodes) { | 
| 1788 | 0 | 0 |  |  |  | 0 | unless($with_hash{$node}) { | 
| 1789 | 0 |  |  |  |  | 0 | push @nodes, $node; | 
| 1790 |  |  |  |  |  |  | } | 
| 1791 |  |  |  |  |  |  | } | 
| 1792 |  |  |  |  |  |  | } | 
| 1793 |  |  |  |  |  |  |  | 
| 1794 | 0 |  |  |  |  | 0 | return @nodes; | 
| 1795 |  |  |  |  |  |  | } | 
| 1796 |  |  |  |  |  |  |  | 
| 1797 |  |  |  |  |  |  | =item B<_get_list_by_metadata_sql> | 
| 1798 |  |  |  |  |  |  |  | 
| 1799 |  |  |  |  |  |  | Return the SQL to do a match by metadata. Should expect the metadata type | 
| 1800 |  |  |  |  |  |  | as the first SQL parameter, and the metadata value as the second. | 
| 1801 |  |  |  |  |  |  |  | 
| 1802 |  |  |  |  |  |  | If possible, should take account of $args{ignore_case} | 
| 1803 |  |  |  |  |  |  |  | 
| 1804 |  |  |  |  |  |  | =cut | 
| 1805 |  |  |  |  |  |  |  | 
| 1806 |  |  |  |  |  |  | sub _get_list_by_metadata_sql { | 
| 1807 |  |  |  |  |  |  | # SQL 99 version | 
| 1808 |  |  |  |  |  |  | #  Can be over-ridden by database-specific subclasses | 
| 1809 | 0 |  |  | 0 |  | 0 | my ($self, %args) = @_; | 
| 1810 | 0 | 0 |  |  |  | 0 | if ( $args{ignore_case} ) { | 
| 1811 | 0 |  |  |  |  | 0 | return "SELECT node.id, node.name " | 
| 1812 |  |  |  |  |  |  | . "FROM node " | 
| 1813 |  |  |  |  |  |  | . "INNER JOIN metadata " | 
| 1814 |  |  |  |  |  |  | . "   ON (node.id = metadata.node_id " | 
| 1815 |  |  |  |  |  |  | . "       AND node.version=metadata.version) " | 
| 1816 |  |  |  |  |  |  | . "WHERE ". $self->_get_lowercase_compare_sql("metadata.metadata_type") | 
| 1817 |  |  |  |  |  |  | . " AND ". $self->_get_lowercase_compare_sql("metadata.metadata_value"); | 
| 1818 |  |  |  |  |  |  | } else { | 
| 1819 | 0 |  |  |  |  | 0 | return "SELECT node.id, node.name " | 
| 1820 |  |  |  |  |  |  | . "FROM node " | 
| 1821 |  |  |  |  |  |  | . "INNER JOIN metadata " | 
| 1822 |  |  |  |  |  |  | . "   ON (node.id = metadata.node_id " | 
| 1823 |  |  |  |  |  |  | . "       AND node.version=metadata.version) " | 
| 1824 |  |  |  |  |  |  | . "WHERE ". $self->_get_casesensitive_compare_sql("metadata.metadata_type") | 
| 1825 |  |  |  |  |  |  | . " AND ". $self->_get_casesensitive_compare_sql("metadata.metadata_value"); | 
| 1826 |  |  |  |  |  |  | } | 
| 1827 |  |  |  |  |  |  | } | 
| 1828 |  |  |  |  |  |  |  | 
| 1829 |  |  |  |  |  |  | =item B<_get_list_by_missing_metadata_sql> | 
| 1830 |  |  |  |  |  |  |  | 
| 1831 |  |  |  |  |  |  | Return the SQL to do a match by missing metadata. Should expect the metadata | 
| 1832 |  |  |  |  |  |  | type as the first SQL parameter. | 
| 1833 |  |  |  |  |  |  |  | 
| 1834 |  |  |  |  |  |  | If possible, should take account of $args{ignore_case} | 
| 1835 |  |  |  |  |  |  |  | 
| 1836 |  |  |  |  |  |  | =cut | 
| 1837 |  |  |  |  |  |  |  | 
| 1838 |  |  |  |  |  |  | sub _get_list_by_missing_metadata_sql { | 
| 1839 |  |  |  |  |  |  | # SQL 99 version | 
| 1840 |  |  |  |  |  |  | #  Can be over-ridden by database-specific subclasses | 
| 1841 | 0 |  |  | 0 |  | 0 | my ($self, %args) = @_; | 
| 1842 |  |  |  |  |  |  |  | 
| 1843 | 0 |  |  |  |  | 0 | my $sql = ""; | 
| 1844 | 0 | 0 |  |  |  | 0 | if ( $args{ignore_case} ) { | 
| 1845 | 0 |  |  |  |  | 0 | $sql = "SELECT node.id, node.name " | 
| 1846 |  |  |  |  |  |  | . "FROM node " | 
| 1847 |  |  |  |  |  |  | . "LEFT OUTER JOIN metadata " | 
| 1848 |  |  |  |  |  |  | . "   ON (node.id = metadata.node_id " | 
| 1849 |  |  |  |  |  |  | . "       AND node.version=metadata.version " | 
| 1850 |  |  |  |  |  |  | . "       AND ". $self->_get_lowercase_compare_sql("metadata.metadata_type") | 
| 1851 |  |  |  |  |  |  | . ")"; | 
| 1852 |  |  |  |  |  |  | } else { | 
| 1853 | 0 |  |  |  |  | 0 | $sql = "SELECT node.id, node.name " | 
| 1854 |  |  |  |  |  |  | . "FROM node " | 
| 1855 |  |  |  |  |  |  | . "LEFT OUTER JOIN metadata " | 
| 1856 |  |  |  |  |  |  | . "   ON (node.id = metadata.node_id " | 
| 1857 |  |  |  |  |  |  | . "       AND node.version=metadata.version " | 
| 1858 |  |  |  |  |  |  | . "       AND ". $self->_get_casesensitive_compare_sql("metadata.metadata_type") | 
| 1859 |  |  |  |  |  |  | . ")"; | 
| 1860 |  |  |  |  |  |  | } | 
| 1861 |  |  |  |  |  |  |  | 
| 1862 | 0 |  |  |  |  | 0 | $sql .= "WHERE (metadata.metadata_value IS NULL OR LENGTH(metadata.metadata_value) = 0) "; | 
| 1863 | 0 |  |  |  |  | 0 | return $sql; | 
| 1864 |  |  |  |  |  |  | } | 
| 1865 |  |  |  |  |  |  |  | 
| 1866 |  |  |  |  |  |  | sub _get_lowercase_compare_sql { | 
| 1867 | 0 |  |  | 0 |  | 0 | my ($self, $column) = @_; | 
| 1868 |  |  |  |  |  |  | # SQL 99 version | 
| 1869 |  |  |  |  |  |  | #  Can be over-ridden by database-specific subclasses | 
| 1870 | 0 |  |  |  |  | 0 | return "lower($column) = ?"; | 
| 1871 |  |  |  |  |  |  | } | 
| 1872 |  |  |  |  |  |  | sub _get_casesensitive_compare_sql { | 
| 1873 | 0 |  |  | 0 |  | 0 | my ($self, $column) = @_; | 
| 1874 |  |  |  |  |  |  | # SQL 99 version | 
| 1875 |  |  |  |  |  |  | #  Can be over-ridden by database-specific subclasses | 
| 1876 | 0 |  |  |  |  | 0 | return "$column = ?"; | 
| 1877 |  |  |  |  |  |  | } | 
| 1878 |  |  |  |  |  |  |  | 
| 1879 |  |  |  |  |  |  | sub _get_comparison_sql { | 
| 1880 | 0 |  |  | 0 |  | 0 | my ($self, %args) = @_; | 
| 1881 |  |  |  |  |  |  | # SQL 99 version | 
| 1882 |  |  |  |  |  |  | #  Can be over-ridden by database-specific subclasses | 
| 1883 | 0 |  |  |  |  | 0 | return "$args{thing1} = $args{thing2}"; | 
| 1884 |  |  |  |  |  |  | } | 
| 1885 |  |  |  |  |  |  |  | 
| 1886 |  |  |  |  |  |  | sub _get_node_exists_ignore_case_sql { | 
| 1887 |  |  |  |  |  |  | # SQL 99 version | 
| 1888 |  |  |  |  |  |  | #  Can be over-ridden by database-specific subclasses | 
| 1889 | 0 |  |  | 0 |  | 0 | return "SELECT name FROM node WHERE name = ? "; | 
| 1890 |  |  |  |  |  |  | } | 
| 1891 |  |  |  |  |  |  |  | 
| 1892 |  |  |  |  |  |  | =item B | 
| 1893 |  |  |  |  |  |  |  | 
| 1894 |  |  |  |  |  |  | my @nodes = $wiki->list_unmoderated_nodes(); | 
| 1895 |  |  |  |  |  |  | my @nodes = $wiki->list_unmoderated_nodes( | 
| 1896 |  |  |  |  |  |  | only_where_latest => 1 | 
| 1897 |  |  |  |  |  |  | ); | 
| 1898 |  |  |  |  |  |  |  | 
| 1899 |  |  |  |  |  |  | $nodes[0]->{'name'}              # The name of the node | 
| 1900 |  |  |  |  |  |  | $nodes[0]->{'node_id'}           # The id of the node | 
| 1901 |  |  |  |  |  |  | $nodes[0]->{'version'}           # The version in need of moderation | 
| 1902 |  |  |  |  |  |  | $nodes[0]->{'moderated_version'} # The newest moderated version | 
| 1903 |  |  |  |  |  |  |  | 
| 1904 |  |  |  |  |  |  | With only_where_latest set, return the id, name and version of all the | 
| 1905 |  |  |  |  |  |  | nodes where the most recent version needs moderation. | 
| 1906 |  |  |  |  |  |  | Otherwise, returns the id, name and version of all node versions that need | 
| 1907 |  |  |  |  |  |  | to be moderated. | 
| 1908 |  |  |  |  |  |  |  | 
| 1909 |  |  |  |  |  |  | =cut | 
| 1910 |  |  |  |  |  |  |  | 
| 1911 |  |  |  |  |  |  | sub list_unmoderated_nodes { | 
| 1912 | 0 |  |  | 0 | 1 | 0 | my ($self,%args) = @_; | 
| 1913 |  |  |  |  |  |  |  | 
| 1914 | 0 |  |  |  |  | 0 | my $only_where_lastest = $args{'only_where_latest'}; | 
| 1915 |  |  |  |  |  |  |  | 
| 1916 | 0 |  |  |  |  | 0 | my $sql = | 
| 1917 |  |  |  |  |  |  | "SELECT " | 
| 1918 |  |  |  |  |  |  | ."    id, name, " | 
| 1919 |  |  |  |  |  |  | ."    node.version AS last_moderated_version, " | 
| 1920 |  |  |  |  |  |  | ."    content.version AS version " | 
| 1921 |  |  |  |  |  |  | ."FROM content " | 
| 1922 |  |  |  |  |  |  | ."INNER JOIN node " | 
| 1923 |  |  |  |  |  |  | ."    ON (id = node_id) " | 
| 1924 |  |  |  |  |  |  | ."WHERE moderated = ? " | 
| 1925 |  |  |  |  |  |  | ; | 
| 1926 | 0 | 0 |  |  |  | 0 | if($only_where_lastest) { | 
| 1927 | 0 |  |  |  |  | 0 | $sql .= "AND node.version = content.version "; | 
| 1928 |  |  |  |  |  |  | } | 
| 1929 | 0 |  |  |  |  | 0 | $sql .= "ORDER BY name, content.version "; | 
| 1930 |  |  |  |  |  |  |  | 
| 1931 |  |  |  |  |  |  | # Query | 
| 1932 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 1933 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare( $sql ); | 
| 1934 | 0 |  |  |  |  | 0 | $sth->execute( "0" ); | 
| 1935 |  |  |  |  |  |  |  | 
| 1936 | 0 |  |  |  |  | 0 | my @nodes; | 
| 1937 | 0 |  |  |  |  | 0 | while(my @results = $sth->fetchrow_array) { | 
| 1938 | 0 |  |  |  |  | 0 | my %data; | 
| 1939 | 0 |  |  |  |  | 0 | @data{ qw( node_id name moderated_version version ) } = @results; | 
| 1940 | 0 |  |  |  |  | 0 | push @nodes, \%data; | 
| 1941 |  |  |  |  |  |  | } | 
| 1942 |  |  |  |  |  |  |  | 
| 1943 | 0 |  |  |  |  | 0 | return @nodes; | 
| 1944 |  |  |  |  |  |  | } | 
| 1945 |  |  |  |  |  |  |  | 
| 1946 |  |  |  |  |  |  | =item B | 
| 1947 |  |  |  |  |  |  |  | 
| 1948 |  |  |  |  |  |  | List the last version of every node before a given date. | 
| 1949 |  |  |  |  |  |  | If no version existed before that date, will return undef for version. | 
| 1950 |  |  |  |  |  |  | Returns a hash of id, name, version and date | 
| 1951 |  |  |  |  |  |  |  | 
| 1952 |  |  |  |  |  |  | my @nv = $wiki->list_last_version_before('2007-01-02 10:34:11') | 
| 1953 |  |  |  |  |  |  | foreach my $data (@nv) { | 
| 1954 |  |  |  |  |  |  |  | 
| 1955 |  |  |  |  |  |  | } | 
| 1956 |  |  |  |  |  |  |  | 
| 1957 |  |  |  |  |  |  | =cut | 
| 1958 |  |  |  |  |  |  |  | 
| 1959 |  |  |  |  |  |  | sub list_last_version_before { | 
| 1960 | 0 |  |  | 0 | 1 | 0 | my ($self, $date) = @_; | 
| 1961 |  |  |  |  |  |  |  | 
| 1962 | 0 |  |  |  |  | 0 | my $sql = | 
| 1963 |  |  |  |  |  |  | "SELECT " | 
| 1964 |  |  |  |  |  |  | ."    id, name, " | 
| 1965 |  |  |  |  |  |  | ."MAX(content.version) AS version, MAX(content.modified) AS modified " | 
| 1966 |  |  |  |  |  |  | ."FROM node " | 
| 1967 |  |  |  |  |  |  | ."LEFT OUTER JOIN content " | 
| 1968 |  |  |  |  |  |  | ."    ON (id = node_id " | 
| 1969 |  |  |  |  |  |  | ."      AND content.modified <= ?) " | 
| 1970 |  |  |  |  |  |  | ."GROUP BY id, name " | 
| 1971 |  |  |  |  |  |  | ."ORDER BY id " | 
| 1972 |  |  |  |  |  |  | ; | 
| 1973 |  |  |  |  |  |  |  | 
| 1974 |  |  |  |  |  |  | # Query | 
| 1975 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 1976 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare( $sql ); | 
| 1977 | 0 |  |  |  |  | 0 | $sth->execute( $date ); | 
| 1978 |  |  |  |  |  |  |  | 
| 1979 | 0 |  |  |  |  | 0 | my @nodes; | 
| 1980 | 0 |  |  |  |  | 0 | while(my @results = $sth->fetchrow_array) { | 
| 1981 | 0 |  |  |  |  | 0 | my %data; | 
| 1982 | 0 |  |  |  |  | 0 | @data{ qw( id name version modified ) } = @results; | 
| 1983 | 0 |  |  |  |  | 0 | $data{'node_id'} = $data{'id'}; | 
| 1984 | 0 | 0 |  |  |  | 0 | unless($data{'version'}) { $data{'version'} = undef; } | 
|  | 0 |  |  |  |  | 0 |  | 
| 1985 | 0 |  |  |  |  | 0 | push @nodes, \%data; | 
| 1986 |  |  |  |  |  |  | } | 
| 1987 |  |  |  |  |  |  |  | 
| 1988 | 0 |  |  |  |  | 0 | return @nodes; | 
| 1989 |  |  |  |  |  |  | } | 
| 1990 |  |  |  |  |  |  |  | 
| 1991 |  |  |  |  |  |  |  | 
| 1992 |  |  |  |  |  |  | # Internal function only, used when querying latest metadata | 
| 1993 |  |  |  |  |  |  | sub _current_node_id_versions { | 
| 1994 | 0 |  |  | 0 |  | 0 | my ($self) = @_; | 
| 1995 |  |  |  |  |  |  |  | 
| 1996 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 1997 |  |  |  |  |  |  |  | 
| 1998 | 0 |  |  |  |  | 0 | my $nv_sql = | 
| 1999 |  |  |  |  |  |  | "SELECT node_id, MAX(version) ". | 
| 2000 |  |  |  |  |  |  | "FROM content ". | 
| 2001 |  |  |  |  |  |  | "WHERE moderated ". | 
| 2002 |  |  |  |  |  |  | "GROUP BY node_id "; | 
| 2003 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare( $nv_sql ); | 
| 2004 | 0 |  |  |  |  | 0 | $sth->execute(); | 
| 2005 |  |  |  |  |  |  |  | 
| 2006 | 0 |  |  |  |  | 0 | my @nv_where; | 
| 2007 | 0 |  |  |  |  | 0 | while(my @results = $sth->fetchrow_array) { | 
| 2008 | 0 |  |  |  |  | 0 | my ($node_id, $version) = @results; | 
| 2009 | 0 |  |  |  |  | 0 | my $where = "(node_id=$node_id AND version=$version)"; | 
| 2010 | 0 |  |  |  |  | 0 | push @nv_where, $where; | 
| 2011 |  |  |  |  |  |  | } | 
| 2012 | 0 |  |  |  |  | 0 | return @nv_where; | 
| 2013 |  |  |  |  |  |  | } | 
| 2014 |  |  |  |  |  |  |  | 
| 2015 |  |  |  |  |  |  | =item B | 
| 2016 |  |  |  |  |  |  |  | 
| 2017 |  |  |  |  |  |  | List all the currently defined values of the given type of metadata. | 
| 2018 |  |  |  |  |  |  |  | 
| 2019 |  |  |  |  |  |  | Will only return data from the latest moderated version of each node | 
| 2020 |  |  |  |  |  |  |  | 
| 2021 |  |  |  |  |  |  | # List all of the different metadata values with the type 'category' | 
| 2022 |  |  |  |  |  |  | my @categories = $wiki->list_metadata_by_type('category'); | 
| 2023 |  |  |  |  |  |  |  | 
| 2024 |  |  |  |  |  |  | =cut | 
| 2025 |  |  |  |  |  |  | sub list_metadata_by_type { | 
| 2026 | 0 |  |  | 0 | 1 | 0 | my ($self, $type) = @_; | 
| 2027 |  |  |  |  |  |  |  | 
| 2028 | 0 | 0 |  |  |  | 0 | return undef unless $type; | 
| 2029 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 2030 |  |  |  |  |  |  |  | 
| 2031 |  |  |  |  |  |  | # Ideally we'd do this as one big query | 
| 2032 |  |  |  |  |  |  | # However, this would need a temporary table on many | 
| 2033 |  |  |  |  |  |  | #  database engines, so we cheat and do it as two | 
| 2034 | 0 |  |  |  |  | 0 | my @nv_where = $self->_current_node_id_versions(); | 
| 2035 |  |  |  |  |  |  |  | 
| 2036 |  |  |  |  |  |  | # Now the metadata bit | 
| 2037 | 0 |  |  |  |  | 0 | my $sql = | 
| 2038 |  |  |  |  |  |  | "SELECT DISTINCT metadata_value ". | 
| 2039 |  |  |  |  |  |  | "FROM metadata ". | 
| 2040 |  |  |  |  |  |  | "WHERE metadata_type = ? ". | 
| 2041 |  |  |  |  |  |  | "AND (". | 
| 2042 |  |  |  |  |  |  | join(" OR ", @nv_where). | 
| 2043 |  |  |  |  |  |  | ")"; | 
| 2044 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare( $sql ); | 
| 2045 | 0 |  |  |  |  | 0 | $sth->execute($type); | 
| 2046 |  |  |  |  |  |  |  | 
| 2047 | 0 |  |  |  |  | 0 | my $values = $sth->fetchall_arrayref([0]); | 
| 2048 | 0 |  |  |  |  | 0 | return ( map { $self->charset_decode( $_->[0] ) } (@$values) ); | 
|  | 0 |  |  |  |  | 0 |  | 
| 2049 |  |  |  |  |  |  | } | 
| 2050 |  |  |  |  |  |  |  | 
| 2051 |  |  |  |  |  |  |  | 
| 2052 |  |  |  |  |  |  | =item B | 
| 2053 |  |  |  |  |  |  |  | 
| 2054 |  |  |  |  |  |  | List all the currently defined kinds of metadata, eg Locale, Postcode | 
| 2055 |  |  |  |  |  |  |  | 
| 2056 |  |  |  |  |  |  | Will only return data from the latest moderated version of each node | 
| 2057 |  |  |  |  |  |  |  | 
| 2058 |  |  |  |  |  |  | # List all of the different kinds of metadata | 
| 2059 |  |  |  |  |  |  | my @metadata_types = $wiki->list_metadata_names() | 
| 2060 |  |  |  |  |  |  |  | 
| 2061 |  |  |  |  |  |  | =cut | 
| 2062 |  |  |  |  |  |  | sub list_metadata_names { | 
| 2063 | 0 |  |  | 0 | 1 | 0 | my ($self) = @_; | 
| 2064 |  |  |  |  |  |  |  | 
| 2065 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 2066 |  |  |  |  |  |  |  | 
| 2067 |  |  |  |  |  |  | # Ideally we'd do this as one big query | 
| 2068 |  |  |  |  |  |  | # However, this would need a temporary table on many | 
| 2069 |  |  |  |  |  |  | #  database engines, so we cheat and do it as two | 
| 2070 | 0 |  |  |  |  | 0 | my @nv_where = $self->_current_node_id_versions(); | 
| 2071 |  |  |  |  |  |  |  | 
| 2072 |  |  |  |  |  |  | # Now the metadata bit | 
| 2073 | 0 |  |  |  |  | 0 | my $sql = | 
| 2074 |  |  |  |  |  |  | "SELECT DISTINCT metadata_type ". | 
| 2075 |  |  |  |  |  |  | "FROM metadata ". | 
| 2076 |  |  |  |  |  |  | "WHERE (". | 
| 2077 |  |  |  |  |  |  | join(" OR ", @nv_where). | 
| 2078 |  |  |  |  |  |  | ")"; | 
| 2079 | 0 |  |  |  |  | 0 | my $sth = $dbh->prepare( $sql ); | 
| 2080 | 0 |  |  |  |  | 0 | $sth->execute(); | 
| 2081 |  |  |  |  |  |  |  | 
| 2082 | 0 |  |  |  |  | 0 | my $types = $sth->fetchall_arrayref([0]); | 
| 2083 | 0 |  |  |  |  | 0 | return ( map { $self->charset_decode( $_->[0] ) } (@$types) ); | 
|  | 0 |  |  |  |  | 0 |  | 
| 2084 |  |  |  |  |  |  | } | 
| 2085 |  |  |  |  |  |  |  | 
| 2086 |  |  |  |  |  |  |  | 
| 2087 |  |  |  |  |  |  | =item B | 
| 2088 |  |  |  |  |  |  |  | 
| 2089 |  |  |  |  |  |  | my ($code_version, $db_version) = $store->schema_current; | 
| 2090 |  |  |  |  |  |  | if ($code_version == $db_version) | 
| 2091 |  |  |  |  |  |  | # Do stuff | 
| 2092 |  |  |  |  |  |  | } else { | 
| 2093 |  |  |  |  |  |  | # Bail | 
| 2094 |  |  |  |  |  |  | } | 
| 2095 |  |  |  |  |  |  |  | 
| 2096 |  |  |  |  |  |  | =cut | 
| 2097 |  |  |  |  |  |  |  | 
| 2098 |  |  |  |  |  |  | sub schema_current { | 
| 2099 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 2100 | 0 |  |  |  |  | 0 | my $dbh = $self->dbh; | 
| 2101 | 0 |  |  |  |  | 0 | my $sth; | 
| 2102 | 0 |  |  |  |  | 0 | eval { $sth = $dbh->prepare("SELECT version FROM schema_info") }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 2103 | 0 | 0 |  |  |  | 0 | if ($@) { | 
| 2104 | 0 |  |  |  |  | 0 | return ($SCHEMA_VER, 0); | 
| 2105 |  |  |  |  |  |  | } | 
| 2106 | 0 |  |  |  |  | 0 | eval { $sth->execute }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 2107 | 0 | 0 |  |  |  | 0 | if ($@) { | 
| 2108 | 0 |  |  |  |  | 0 | return ($SCHEMA_VER, 0); | 
| 2109 |  |  |  |  |  |  | } | 
| 2110 | 0 |  |  |  |  | 0 | my $version; | 
| 2111 | 0 |  |  |  |  | 0 | eval { $version = $sth->fetchrow_array }; | 
|  | 0 |  |  |  |  | 0 |  | 
| 2112 | 0 | 0 |  |  |  | 0 | if ($@) { | 
| 2113 | 0 |  |  |  |  | 0 | return ($SCHEMA_VER, 0); | 
| 2114 |  |  |  |  |  |  | } else { | 
| 2115 | 0 |  |  |  |  | 0 | return ($SCHEMA_VER, $version); | 
| 2116 |  |  |  |  |  |  | } | 
| 2117 |  |  |  |  |  |  | } | 
| 2118 |  |  |  |  |  |  |  | 
| 2119 |  |  |  |  |  |  |  | 
| 2120 |  |  |  |  |  |  | =item B | 
| 2121 |  |  |  |  |  |  |  | 
| 2122 |  |  |  |  |  |  | my $dbh = $store->dbh; | 
| 2123 |  |  |  |  |  |  |  | 
| 2124 |  |  |  |  |  |  | Returns the database handle belonging to this storage backend instance. | 
| 2125 |  |  |  |  |  |  |  | 
| 2126 |  |  |  |  |  |  | =cut | 
| 2127 |  |  |  |  |  |  |  | 
| 2128 |  |  |  |  |  |  | sub dbh { | 
| 2129 | 3 |  |  | 3 | 1 | 7 | my $self = shift; | 
| 2130 | 3 |  |  |  |  | 8 | return $self->{_dbh}; | 
| 2131 |  |  |  |  |  |  | } | 
| 2132 |  |  |  |  |  |  |  | 
| 2133 |  |  |  |  |  |  | =item B | 
| 2134 |  |  |  |  |  |  |  | 
| 2135 |  |  |  |  |  |  | my $dbname = $store->dbname; | 
| 2136 |  |  |  |  |  |  |  | 
| 2137 |  |  |  |  |  |  | Returns the name of the database used for backend storage. | 
| 2138 |  |  |  |  |  |  |  | 
| 2139 |  |  |  |  |  |  | =cut | 
| 2140 |  |  |  |  |  |  |  | 
| 2141 |  |  |  |  |  |  | sub dbname { | 
| 2142 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 2143 | 0 |  |  |  |  | 0 | return $self->{_dbname}; | 
| 2144 |  |  |  |  |  |  | } | 
| 2145 |  |  |  |  |  |  |  | 
| 2146 |  |  |  |  |  |  | =item B | 
| 2147 |  |  |  |  |  |  |  | 
| 2148 |  |  |  |  |  |  | my $dbuser = $store->dbuser; | 
| 2149 |  |  |  |  |  |  |  | 
| 2150 |  |  |  |  |  |  | Returns the username used to connect to the database used for backend storage. | 
| 2151 |  |  |  |  |  |  |  | 
| 2152 |  |  |  |  |  |  | =cut | 
| 2153 |  |  |  |  |  |  |  | 
| 2154 |  |  |  |  |  |  | sub dbuser { | 
| 2155 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 2156 | 0 |  |  |  |  | 0 | return $self->{_dbuser}; | 
| 2157 |  |  |  |  |  |  | } | 
| 2158 |  |  |  |  |  |  |  | 
| 2159 |  |  |  |  |  |  | =item B | 
| 2160 |  |  |  |  |  |  |  | 
| 2161 |  |  |  |  |  |  | my $dbpass = $store->dbpass; | 
| 2162 |  |  |  |  |  |  |  | 
| 2163 |  |  |  |  |  |  | Returns the password used to connect to the database used for backend storage. | 
| 2164 |  |  |  |  |  |  |  | 
| 2165 |  |  |  |  |  |  | =cut | 
| 2166 |  |  |  |  |  |  |  | 
| 2167 |  |  |  |  |  |  | sub dbpass { | 
| 2168 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 2169 | 0 |  |  |  |  | 0 | return $self->{_dbpass}; | 
| 2170 |  |  |  |  |  |  | } | 
| 2171 |  |  |  |  |  |  |  | 
| 2172 |  |  |  |  |  |  | =item B | 
| 2173 |  |  |  |  |  |  |  | 
| 2174 |  |  |  |  |  |  | my $dbhost = $store->dbhost; | 
| 2175 |  |  |  |  |  |  |  | 
| 2176 |  |  |  |  |  |  | Returns the optional host used to connect to the database used for | 
| 2177 |  |  |  |  |  |  | backend storage. | 
| 2178 |  |  |  |  |  |  |  | 
| 2179 |  |  |  |  |  |  | =cut | 
| 2180 |  |  |  |  |  |  |  | 
| 2181 |  |  |  |  |  |  | sub dbhost { | 
| 2182 | 0 |  |  | 0 | 1 | 0 | my $self = shift; | 
| 2183 | 0 |  |  |  |  | 0 | return $self->{_dbhost}; | 
| 2184 |  |  |  |  |  |  | } | 
| 2185 |  |  |  |  |  |  |  | 
| 2186 |  |  |  |  |  |  | # Cleanup. | 
| 2187 |  |  |  |  |  |  | sub DESTROY { | 
| 2188 | 3 |  |  | 3 |  | 8 | my $self = shift; | 
| 2189 | 3 | 50 |  |  |  | 22 | return if $self->{_external_dbh}; | 
| 2190 | 3 |  |  |  |  | 18 | my $dbh = $self->dbh; | 
| 2191 | 3 | 50 |  |  |  | 23 | $dbh->disconnect if $dbh; | 
| 2192 |  |  |  |  |  |  | } | 
| 2193 |  |  |  |  |  |  |  | 
| 2194 |  |  |  |  |  |  | # decode a string of octets into perl's internal encoding, based on the | 
| 2195 |  |  |  |  |  |  | # charset parameter we were passed. Takes a list, returns a list. | 
| 2196 |  |  |  |  |  |  | sub charset_decode { | 
| 2197 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 2198 | 0 |  |  |  |  |  | my @input = @_; | 
| 2199 | 0 | 0 |  |  |  |  | if ($CAN_USE_ENCODE) { | 
| 2200 | 0 |  |  |  |  |  | my @output; | 
| 2201 | 0 |  |  |  |  |  | for (@input) { | 
| 2202 | 0 |  |  |  |  |  | push( @output, Encode::decode( $self->{_charset}, $_ ) ); | 
| 2203 |  |  |  |  |  |  | } | 
| 2204 | 0 |  |  |  |  |  | return @output; | 
| 2205 |  |  |  |  |  |  | } | 
| 2206 | 0 |  |  |  |  |  | return @input; | 
| 2207 |  |  |  |  |  |  | } | 
| 2208 |  |  |  |  |  |  |  | 
| 2209 |  |  |  |  |  |  | # convert a perl string into a series of octets we can put into the database | 
| 2210 |  |  |  |  |  |  | # takes a list, returns a list | 
| 2211 |  |  |  |  |  |  | sub charset_encode { | 
| 2212 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 2213 | 0 |  |  |  |  |  | my @input = @_; | 
| 2214 | 0 | 0 |  |  |  |  | if ($CAN_USE_ENCODE) { | 
| 2215 | 0 |  |  |  |  |  | my @output; | 
| 2216 | 0 |  |  |  |  |  | for (@input) { | 
| 2217 | 0 |  |  |  |  |  | push( @output, Encode::encode( $self->{_charset}, $_ ) ); | 
| 2218 |  |  |  |  |  |  | } | 
| 2219 | 0 |  |  |  |  |  | return @output; | 
| 2220 |  |  |  |  |  |  | } | 
| 2221 | 0 |  |  |  |  |  | return @input; | 
| 2222 |  |  |  |  |  |  | } | 
| 2223 |  |  |  |  |  |  |  | 
| 2224 |  |  |  |  |  |  | =back | 
| 2225 |  |  |  |  |  |  |  | 
| 2226 |  |  |  |  |  |  | =cut | 
| 2227 |  |  |  |  |  |  |  | 
| 2228 |  |  |  |  |  |  | 1; |