| 1 | package Wiki::Toolkit; |
|---|
| 2 | |
|---|
| 3 | use strict; |
|---|
| 4 | |
|---|
| 5 | use vars qw( $VERSION ); |
|---|
| 6 | $VERSION = '0.75'; |
|---|
| 7 | |
|---|
| 8 | use Carp qw(croak carp); |
|---|
| 9 | use Digest::MD5 "md5_hex"; |
|---|
| 10 | |
|---|
| 11 | # first, detect if Encode is available - it's not under 5.6. If we _are_ |
|---|
| 12 | # under 5.6, give up - we'll just have to hope that nothing explodes. This |
|---|
| 13 | # is the current 0.54 behaviour, so that's ok. |
|---|
| 14 | |
|---|
| 15 | my $CAN_USE_ENCODE; |
|---|
| 16 | BEGIN { |
|---|
| 17 | eval " use Encode "; |
|---|
| 18 | $CAN_USE_ENCODE = $@ ? 0 : 1; |
|---|
| 19 | } |
|---|
| 20 | |
|---|
| 21 | |
|---|
| 22 | =head1 NAME |
|---|
| 23 | |
|---|
| 24 | Wiki::Toolkit - A toolkit for building Wikis. |
|---|
| 25 | |
|---|
| 26 | =head1 DESCRIPTION |
|---|
| 27 | |
|---|
| 28 | Helps you develop Wikis quickly by taking care of the boring bits for |
|---|
| 29 | you. You will still need to write some code - this isn't an instant Wiki. |
|---|
| 30 | |
|---|
| 31 | =head1 SYNOPSIS |
|---|
| 32 | |
|---|
| 33 | # Set up a wiki object with an SQLite storage backend, and an |
|---|
| 34 | # inverted index/DB_File search backend. This store/search |
|---|
| 35 | # combination can be used on systems with no access to an actual |
|---|
| 36 | # database server. |
|---|
| 37 | |
|---|
| 38 | my $store = Wiki::Toolkit::Store::SQLite->new( |
|---|
| 39 | dbname => "/home/wiki/store.db" ); |
|---|
| 40 | my $indexdb = Search::InvertedIndex::DB::DB_File_SplitHash->new( |
|---|
| 41 | -map_name => "/home/wiki/indexes.db", |
|---|
| 42 | -lock_mode => "EX" ); |
|---|
| 43 | my $search = Wiki::Toolkit::Search::SII->new( |
|---|
| 44 | indexdb => $indexdb ); |
|---|
| 45 | |
|---|
| 46 | my $wiki = Wiki::Toolkit->new( store => $store, |
|---|
| 47 | search => $search ); |
|---|
| 48 | |
|---|
| 49 | # Do all the CGI stuff. |
|---|
| 50 | my $q = CGI->new; |
|---|
| 51 | my $action = $q->param("action"); |
|---|
| 52 | my $node = $q->param("node"); |
|---|
| 53 | |
|---|
| 54 | if ($action eq 'display') { |
|---|
| 55 | my $raw = $wiki->retrieve_node($node); |
|---|
| 56 | my $cooked = $wiki->format($raw); |
|---|
| 57 | print_page(node => $node, |
|---|
| 58 | content => $cooked); |
|---|
| 59 | } elsif ($action eq 'preview') { |
|---|
| 60 | my $submitted_content = $q->param("content"); |
|---|
| 61 | my $preview_html = $wiki->format($submitted_content); |
|---|
| 62 | print_editform(node => $node, |
|---|
| 63 | content => $submitted_content, |
|---|
| 64 | preview => $preview_html); |
|---|
| 65 | } elsif ($action eq 'commit') { |
|---|
| 66 | my $submitted_content = $q->param("content"); |
|---|
| 67 | my $cksum = $q->param("checksum"); |
|---|
| 68 | my $written = $wiki->write_node($node, $submitted_content, $cksum); |
|---|
| 69 | if ($written) { |
|---|
| 70 | print_success($node); |
|---|
| 71 | } else { |
|---|
| 72 | handle_conflict($node, $submitted_content); |
|---|
| 73 | } |
|---|
| 74 | } |
|---|
| 75 | |
|---|
| 76 | =head1 METHODS |
|---|
| 77 | |
|---|
| 78 | =over 4 |
|---|
| 79 | |
|---|
| 80 | =item B<new> |
|---|
| 81 | |
|---|
| 82 | # Set up store, search and formatter objects. |
|---|
| 83 | my $store = Wiki::Toolkit::Store::SQLite->new( |
|---|
| 84 | dbname => "/home/wiki/store.db" ); |
|---|
| 85 | my $indexdb = Search::InvertedIndex::DB::DB_File_SplitHash->new( |
|---|
| 86 | -map_name => "/home/wiki/indexes.db", |
|---|
| 87 | -lock_mode => "EX" ); |
|---|
| 88 | my $search = Wiki::Toolkit::Search::SII->new( |
|---|
| 89 | indexdb => $indexdb ); |
|---|
| 90 | my $formatter = My::HomeMade::Formatter->new; |
|---|
| 91 | |
|---|
| 92 | my $wiki = Wiki::Toolkit->new( |
|---|
| 93 | store => $store, # mandatory |
|---|
| 94 | search => $search, # defaults to undef |
|---|
| 95 | formatter => $formatter # defaults to something suitable |
|---|
| 96 | ); |
|---|
| 97 | |
|---|
| 98 | C<store> must be an object of type C<Wiki::Toolkit::Store::*> and |
|---|
| 99 | C<search> if supplied must be of type C<Wiki::Toolkit::Search::*> (though |
|---|
| 100 | this isn't checked yet - FIXME). If C<formatter> isn't supplied, it |
|---|
| 101 | defaults to an object of class L<Wiki::Toolkit::Formatter::Default>. |
|---|
| 102 | |
|---|
| 103 | You can get a searchable Wiki up and running on a system without an |
|---|
| 104 | actual database server by using the SQLite storage backend with the |
|---|
| 105 | SII/DB_File search backend - cut and paste the lines above for a quick |
|---|
| 106 | start, and see L<Wiki::Toolkit::Store::SQLite>, L<Wiki::Toolkit::Search::SII>, |
|---|
| 107 | and L<Search::InvertedIndex::DB::DB_File_SplitHash> when you want to |
|---|
| 108 | learn the details. |
|---|
| 109 | |
|---|
| 110 | C<formatter> can be any object that behaves in the right way; this |
|---|
| 111 | essentially means that it needs to provide a C<format> method which |
|---|
| 112 | takes in raw text and returns the formatted version. See |
|---|
| 113 | L<Wiki::Toolkit::Formatter::Default> for a simple example. Note that you can |
|---|
| 114 | create a suitable object from a sub very quickly by using |
|---|
| 115 | L<Test::MockObject> like so: |
|---|
| 116 | |
|---|
| 117 | my $formatter = Test::MockObject->new(); |
|---|
| 118 | $formatter->mock( 'format', sub { my ($self, $raw) = @_; |
|---|
| 119 | return uc( $raw ); |
|---|
| 120 | } ); |
|---|
| 121 | |
|---|
| 122 | I'm not sure whether to put this in the module or not - it'd let you |
|---|
| 123 | just supply a sub instead of an object as the formatter, but it feels |
|---|
| 124 | wrong to be using a Test::* module in actual code. |
|---|
| 125 | |
|---|
| 126 | =cut |
|---|
| 127 | |
|---|
| 128 | sub new { |
|---|
| 129 | my ($class, @args) = @_; |
|---|
| 130 | my $self = {}; |
|---|
| 131 | bless $self, $class; |
|---|
| 132 | $self->_init(@args) or return undef; |
|---|
| 133 | return $self; |
|---|
| 134 | } |
|---|
| 135 | |
|---|
| 136 | sub _init { |
|---|
| 137 | my ($self, %args) = @_; |
|---|
| 138 | |
|---|
| 139 | # Check for scripts written with old versions of Wiki::Toolkit |
|---|
| 140 | foreach my $obsolete_param ( qw( storage_backend search_backend ) ) { |
|---|
| 141 | carp "You seem to be using a script written for a pre-0.10 version " |
|---|
| 142 | . "of Wiki::Toolkit - the $obsolete_param parameter is no longer used. " |
|---|
| 143 | . "Please read the documentation with 'perldoc Wiki::Toolkit'" |
|---|
| 144 | if $args{$obsolete_param}; |
|---|
| 145 | } |
|---|
| 146 | |
|---|
| 147 | croak "No store supplied" unless $args{store}; |
|---|
| 148 | |
|---|
| 149 | foreach my $k ( qw( store search formatter ) ) { |
|---|
| 150 | $self->{"_".$k} = $args{$k}; |
|---|
| 151 | } |
|---|
| 152 | |
|---|
| 153 | # Make a default formatter object if none was actually supplied. |
|---|
| 154 | unless ( $args{formatter} ) { |
|---|
| 155 | require Wiki::Toolkit::Formatter::Default; |
|---|
| 156 | # Ensure backwards compatibility - versions prior to 0.11 allowed the |
|---|
| 157 | # following options to alter the default behaviour of Text::WikiFormat. |
|---|
| 158 | my %config; |
|---|
| 159 | foreach ( qw( extended_links implicit_links allowed_tags |
|---|
| 160 | macros node_prefix ) ) { |
|---|
| 161 | $config{$_} = $args{$_} if defined $args{$_}; |
|---|
| 162 | } |
|---|
| 163 | $self->{_formatter} = Wiki::Toolkit::Formatter::Default->new( %config ); |
|---|
| 164 | } |
|---|
| 165 | |
|---|
| 166 | # Make a place to store plugins. |
|---|
| 167 | $self->{_registered_plugins} = [ ]; |
|---|
| 168 | |
|---|
| 169 | return $self; |
|---|
| 170 | } |
|---|
| 171 | |
|---|
| 172 | =item B<retrieve_node> |
|---|
| 173 | |
|---|
| 174 | my $content = $wiki->retrieve_node($node); |
|---|
| 175 | |
|---|
| 176 | # Or get additional data about the node as well. |
|---|
| 177 | my %node = $wiki->retrieve_node("HomePage"); |
|---|
| 178 | print "Current Version: " . $node{version}; |
|---|
| 179 | |
|---|
| 180 | # Maybe we stored some of our own custom metadata too. |
|---|
| 181 | my $categories = $node{metadata}{category}; |
|---|
| 182 | print "Categories: " . join(", ", @$categories); |
|---|
| 183 | print "Postcode: $node{metadata}{postcode}[0]"; |
|---|
| 184 | |
|---|
| 185 | # Or get an earlier version: |
|---|
| 186 | my %node = $wiki->retrieve_node( name => "HomePage", |
|---|
| 187 | version => 2, |
|---|
| 188 | ); |
|---|
| 189 | print $node{content}; |
|---|
| 190 | |
|---|
| 191 | In scalar context, returns the current (raw Wiki language) contents of |
|---|
| 192 | the specified node. In list context, returns a hash containing the |
|---|
| 193 | contents of the node plus additional data: |
|---|
| 194 | |
|---|
| 195 | =over 4 |
|---|
| 196 | |
|---|
| 197 | =item B<last_modified> |
|---|
| 198 | |
|---|
| 199 | =item B<version> |
|---|
| 200 | |
|---|
| 201 | =item B<checksum> |
|---|
| 202 | |
|---|
| 203 | =item B<metadata> - a reference to a hash containing any caller-supplied |
|---|
| 204 | metadata sent along the last time the node was written |
|---|
| 205 | |
|---|
| 206 | =back |
|---|
| 207 | |
|---|
| 208 | The C<node> parameter is mandatory. The C<version> parameter is |
|---|
| 209 | optional and defaults to the newest version. If the node hasn't been |
|---|
| 210 | created yet, it is considered to exist but be empty (this behaviour |
|---|
| 211 | might change). |
|---|
| 212 | |
|---|
| 213 | B<Note> on metadata - each hash value is returned as an array ref, |
|---|
| 214 | even if that type of metadata only has one value. |
|---|
| 215 | |
|---|
| 216 | =cut |
|---|
| 217 | |
|---|
| 218 | sub retrieve_node { |
|---|
| 219 | my ($self, @rawargs) = @_; |
|---|
| 220 | |
|---|
| 221 | my %args = scalar @rawargs == 1 ? ( name => $rawargs[0] ) : @rawargs; |
|---|
| 222 | |
|---|
| 223 | my @plugins = $self->get_registered_plugins; |
|---|
| 224 | $args{plugins} = \@plugins if scalar @plugins; |
|---|
| 225 | |
|---|
| 226 | $self->store->retrieve_node( %args ); |
|---|
| 227 | } |
|---|
| 228 | |
|---|
| 229 | =item B<moderate_node> |
|---|
| 230 | |
|---|
| 231 | my $ok = $wiki->moderate_node(name => $node, version => $version); |
|---|
| 232 | |
|---|
| 233 | Marks the given version of the node as moderated. If this is the |
|---|
| 234 | highest moderated version, then update the node's contents to hold |
|---|
| 235 | this version. |
|---|
| 236 | =cut |
|---|
| 237 | |
|---|
| 238 | sub moderate_node { |
|---|
| 239 | my ($self, %args) = @_; |
|---|
| 240 | my @plugins = $self->get_registered_plugins; |
|---|
| 241 | $args{plugins} = \@plugins if scalar @plugins; |
|---|
| 242 | |
|---|
| 243 | my $ret = $self->store->moderate_node( %args ); |
|---|
| 244 | if($ret == -1) { return $ret; } |
|---|
| 245 | return 1; |
|---|
| 246 | } |
|---|
| 247 | |
|---|
| 248 | =item B<set_node_moderation> |
|---|
| 249 | |
|---|
| 250 | my $ok = $wiki->set_node_moderation(name => $node, required => $required); |
|---|
| 251 | |
|---|
| 252 | Sets if a node requires moderation or not. |
|---|
| 253 | (Moderation is required when $required is true). |
|---|
| 254 | |
|---|
| 255 | When moderation is required, new versions of a node will sit about |
|---|
| 256 | until they're tagged as moderated, when they will become the new node. |
|---|
| 257 | =cut |
|---|
| 258 | |
|---|
| 259 | sub set_node_moderation { |
|---|
| 260 | my ($self, @args) = @_; |
|---|
| 261 | return $self->store->set_node_moderation( @args ); |
|---|
| 262 | } |
|---|
| 263 | |
|---|
| 264 | =item B<rename_node> |
|---|
| 265 | |
|---|
| 266 | my $ok = $wiki->rename_node(old_name => $old_name, new_name => $new_name, create_new_versions => $create_new_versions ); |
|---|
| 267 | |
|---|
| 268 | Renames a node, updating any references to it as required. |
|---|
| 269 | |
|---|
| 270 | Uses the internal_links table to identify the nodes that link to this |
|---|
| 271 | one, and re-writes any wiki links in these to point to the new name. If |
|---|
| 272 | required, it can mark these updates to other pages as a new version. |
|---|
| 273 | =cut |
|---|
| 274 | |
|---|
| 275 | sub rename_node { |
|---|
| 276 | my ($self, @argsarray) = @_; |
|---|
| 277 | my %args = @argsarray; |
|---|
| 278 | if((scalar @argsarray) == 2 || (scalar @argsarray) == 3) { |
|---|
| 279 | # Missing keys |
|---|
| 280 | %args = ( |
|---|
| 281 | old_name => $argsarray[0], |
|---|
| 282 | new_name => $argsarray[1], |
|---|
| 283 | create_new_versions => $argsarray[2] |
|---|
| 284 | ); |
|---|
| 285 | } |
|---|
| 286 | |
|---|
| 287 | my @plugins = $self->get_registered_plugins; |
|---|
| 288 | $args{plugins} = \@plugins if scalar @plugins; |
|---|
| 289 | $args{wiki} = $self; |
|---|
| 290 | |
|---|
| 291 | my $ret = $self->store->rename_node( %args ); |
|---|
| 292 | |
|---|
| 293 | if($ret && $ret == -1) { return $ret; } |
|---|
| 294 | return 1; |
|---|
| 295 | } |
|---|
| 296 | |
|---|
| 297 | =item B<verify_checksum> |
|---|
| 298 | |
|---|
| 299 | my $ok = $wiki->verify_checksum($node, $checksum); |
|---|
| 300 | |
|---|
| 301 | Sees whether your checksum is current for the given node. Returns true |
|---|
| 302 | if so, false if not. |
|---|
| 303 | |
|---|
| 304 | B<NOTE:> Be aware that when called directly and without locking, this |
|---|
| 305 | might not be accurate, since there is a small window between the |
|---|
| 306 | checking and the returning where the node might be changed, so |
|---|
| 307 | B<don't> rely on it for safe commits; use C<write_node> for that. It |
|---|
| 308 | can however be useful when previewing edits, for example. |
|---|
| 309 | |
|---|
| 310 | =cut |
|---|
| 311 | |
|---|
| 312 | sub verify_checksum { |
|---|
| 313 | my ($self, @args) = @_; |
|---|
| 314 | $self->store->verify_checksum( @args ); |
|---|
| 315 | } |
|---|
| 316 | |
|---|
| 317 | =item B<list_backlinks> |
|---|
| 318 | |
|---|
| 319 | # List all nodes that link to the Home Page. |
|---|
| 320 | my @links = $wiki->list_backlinks( node => "Home Page" ); |
|---|
| 321 | |
|---|
| 322 | =cut |
|---|
| 323 | |
|---|
| 324 | sub list_backlinks { |
|---|
| 325 | my ($self, @args) = @_; |
|---|
| 326 | $self->store->list_backlinks( @args ); |
|---|
| 327 | } |
|---|
| 328 | |
|---|
| 329 | =item B<list_dangling_links> |
|---|
| 330 | |
|---|
| 331 | # List all nodes that have been linked to from other nodes but don't |
|---|
| 332 | # yet exist. |
|---|
| 333 | my @links = $wiki->list_dangling_links; |
|---|
| 334 | |
|---|
| 335 | Each node is returned once only, regardless of how many other nodes |
|---|
| 336 | link to it. |
|---|
| 337 | |
|---|
| 338 | =cut |
|---|
| 339 | |
|---|
| 340 | sub list_dangling_links { |
|---|
| 341 | my ($self, @args) = @_; |
|---|
| 342 | $self->store->list_dangling_links( @args ); |
|---|
| 343 | } |
|---|
| 344 | |
|---|
| 345 | =item B<list_all_nodes> |
|---|
| 346 | |
|---|
| 347 | my @nodes = $wiki->list_all_nodes; |
|---|
| 348 | |
|---|
| 349 | Returns a list containing the name of every existing node. The list |
|---|
| 350 | won't be in any kind of order; do any sorting in your calling script. |
|---|
| 351 | |
|---|
| 352 | =cut |
|---|
| 353 | |
|---|
| 354 | sub list_all_nodes { |
|---|
| 355 | my ($self, @args) = @_; |
|---|
| 356 | $self->store->list_all_nodes( @args ); |
|---|
| 357 | } |
|---|
| 358 | |
|---|
| 359 | =item B<list_nodes_by_metadata> |
|---|
| 360 | |
|---|
| 361 | # All documentation nodes. |
|---|
| 362 | my @nodes = $wiki->list_nodes_by_metadata( |
|---|
| 363 | metadata_type => "category", |
|---|
| 364 | metadata_value => "documentation", |
|---|
| 365 | ignore_case => 1, # optional but recommended (see below) |
|---|
| 366 | ); |
|---|
| 367 | |
|---|
| 368 | # All pubs in Hammersmith. |
|---|
| 369 | my @pubs = $wiki->list_nodes_by_metadata( |
|---|
| 370 | metadata_type => "category", |
|---|
| 371 | metadata_value => "Pub", |
|---|
| 372 | ); |
|---|
| 373 | my @hsm = $wiki->list_nodes_by_metadata( |
|---|
| 374 | metadata_type => "category", |
|---|
| 375 | metadata_value => "Hammersmith", |
|---|
| 376 | ); |
|---|
| 377 | my @results = my_l33t_method_for_ANDing_arrays( \@pubs, \@hsm ); |
|---|
| 378 | |
|---|
| 379 | Returns a list containing the name of every node whose caller-supplied |
|---|
| 380 | metadata matches the criteria given in the parameters. |
|---|
| 381 | |
|---|
| 382 | By default, the case-sensitivity of both C<metadata_type> and |
|---|
| 383 | C<metadata_value> depends on your database - if it will return rows |
|---|
| 384 | with an attribute value of "Pubs" when you asked for "pubs", or not. |
|---|
| 385 | If you supply a true value to the C<ignore_case> parameter, then you |
|---|
| 386 | can be sure of its being case-insensitive. This is recommended. |
|---|
| 387 | |
|---|
| 388 | If you don't supply any criteria then you'll get an empty list. |
|---|
| 389 | |
|---|
| 390 | This is a really really really simple way of finding things; if you |
|---|
| 391 | want to be more complicated then you'll need to call the method |
|---|
| 392 | multiple times and combine the results yourself, or write a plugin. |
|---|
| 393 | |
|---|
| 394 | =cut |
|---|
| 395 | |
|---|
| 396 | sub list_nodes_by_metadata { |
|---|
| 397 | my ($self, @args) = @_; |
|---|
| 398 | $self->store->list_nodes_by_metadata( @args ); |
|---|
| 399 | } |
|---|
| 400 | |
|---|
| 401 | =item B<list_nodes_by_missing_metadata> |
|---|
| 402 | Returns nodes where either the metadata doesn't exist, or is blank |
|---|
| 403 | |
|---|
| 404 | Unlike list_nodes_by_metadata(), the metadata value is optional (the |
|---|
| 405 | metadata type is required). |
|---|
| 406 | |
|---|
| 407 | # All nodes missing documentation |
|---|
| 408 | my @nodes = $store->list_nodes_by_missing_metadata( |
|---|
| 409 | metadata_type => "category", |
|---|
| 410 | metadata_value => "documentation", |
|---|
| 411 | ignore_case => 1, # optional but recommended (see below) |
|---|
| 412 | ); |
|---|
| 413 | |
|---|
| 414 | # All nodes which don't have a latitude defined |
|---|
| 415 | my @nodes = $store->list_nodes_by_missing_metadata( |
|---|
| 416 | metadata_type => "latitude" |
|---|
| 417 | ); |
|---|
| 418 | =cut |
|---|
| 419 | sub list_nodes_by_missing_metadata { |
|---|
| 420 | my ($self, @args) = @_; |
|---|
| 421 | $self->store->list_nodes_by_missing_metadata( @args ); |
|---|
| 422 | } |
|---|
| 423 | |
|---|
| 424 | =item B<list_recent_changes> |
|---|
| 425 | |
|---|
| 426 | # Nodes changed in last 7 days - each node listed only once. |
|---|
| 427 | my @nodes = $wiki->list_recent_changes( days => 7 ); |
|---|
| 428 | |
|---|
| 429 | # All changes in last 7 days - nodes changed more than once will |
|---|
| 430 | # be listed more than once. |
|---|
| 431 | my @nodes = $wiki->list_recent_changes( |
|---|
| 432 | days => 7, |
|---|
| 433 | include_all_changes => 1, |
|---|
| 434 | ); |
|---|
| 435 | |
|---|
| 436 | # Nodes changed between 1 and 7 days ago. |
|---|
| 437 | my @nodes = $wiki->list_recent_changes( between_days => [ 1, 7 ] ); |
|---|
| 438 | |
|---|
| 439 | # Changes since a given time. |
|---|
| 440 | my @nodes = $wiki->list_recent_changes( since => 1036235131 ); |
|---|
| 441 | |
|---|
| 442 | # Most recent change and its details. |
|---|
| 443 | my @nodes = $wiki->list_recent_changes( last_n_changes => 1 ); |
|---|
| 444 | print "Node: $nodes[0]{name}"; |
|---|
| 445 | print "Last modified: $nodes[0]{last_modified}"; |
|---|
| 446 | print "Comment: $nodes[0]{metadata}{comment}"; |
|---|
| 447 | |
|---|
| 448 | # Last 5 restaurant nodes edited. |
|---|
| 449 | my @nodes = $wiki->list_recent_changes( |
|---|
| 450 | last_n_changes => 5, |
|---|
| 451 | metadata_is => { category => "Restaurants" } |
|---|
| 452 | ); |
|---|
| 453 | |
|---|
| 454 | # Last 5 nodes edited by Kake. |
|---|
| 455 | my @nodes = $wiki->list_recent_changes( |
|---|
| 456 | last_n_changes => 5, |
|---|
| 457 | metadata_was => { username => "Kake" } |
|---|
| 458 | ); |
|---|
| 459 | |
|---|
| 460 | # All minor edits made by Earle in the last week. |
|---|
| 461 | my @nodes = $wiki->list_recent_changes( |
|---|
| 462 | days => 7, |
|---|
| 463 | metadata_was => { username => "Earle", |
|---|
| 464 | edit_type => "Minor tidying." } |
|---|
| 465 | ); |
|---|
| 466 | |
|---|
| 467 | # Last 10 changes that weren't minor edits. |
|---|
| 468 | my @nodes = $wiki->list_recent_changes( |
|---|
| 469 | last_n_changes => 5, |
|---|
| 470 | metadata_wasnt => { edit_type => "Minor tidying" } |
|---|
| 471 | ); |
|---|
| 472 | |
|---|
| 473 | You I<must> supply one of the following constraints: C<days> |
|---|
| 474 | (integer), C<since> (epoch), C<last_n_changes> (integer). |
|---|
| 475 | |
|---|
| 476 | You I<may> also supply I<either> C<metadata_is> (and optionally |
|---|
| 477 | C<metadata_isnt>), I<or> C<metadata_was> (and optionally |
|---|
| 478 | C<metadata_wasnt>). Each of these should be a ref to a hash with |
|---|
| 479 | scalar keys and values. If the hash has more than one entry, then |
|---|
| 480 | only changes satisfying I<all> criteria will be returned when using |
|---|
| 481 | C<metadata_is> or C<metadata_was>, but all changes which fail to |
|---|
| 482 | satisfy any one of the criteria will be returned when using |
|---|
| 483 | C<metadata_isnt> or C<metadata_is>. |
|---|
| 484 | |
|---|
| 485 | C<metadata_is> and C<metadata_isnt> look only at the metadata that the |
|---|
| 486 | node I<currently> has. C<metadata_was> and C<metadata_wasnt> take into |
|---|
| 487 | account the metadata of previous versions of a node. |
|---|
| 488 | |
|---|
| 489 | Returns results as an array, in reverse chronological order. Each |
|---|
| 490 | element of the array is a reference to a hash with the following entries: |
|---|
| 491 | |
|---|
| 492 | =over 4 |
|---|
| 493 | |
|---|
| 494 | =item * B<name>: the name of the node |
|---|
| 495 | |
|---|
| 496 | =item * B<version>: the latest version number |
|---|
| 497 | |
|---|
| 498 | =item * B<last_modified>: the timestamp of when it was last modified |
|---|
| 499 | |
|---|
| 500 | =item * B<metadata>: a ref to a hash containing any metadata attached |
|---|
| 501 | to the current version of the node |
|---|
| 502 | |
|---|
| 503 | =back |
|---|
| 504 | |
|---|
| 505 | Unless you supply C<include_all_changes>, C<metadata_was> or |
|---|
| 506 | C<metadata_wasnt>, each node will only be returned once regardless of |
|---|
| 507 | how many times it has been changed recently. |
|---|
| 508 | |
|---|
| 509 | By default, the case-sensitivity of both C<metadata_type> and |
|---|
| 510 | C<metadata_value> depends on your database - if it will return rows |
|---|
| 511 | with an attribute value of "Pubs" when you asked for "pubs", or not. |
|---|
| 512 | If you supply a true value to the C<ignore_case> parameter, then you |
|---|
| 513 | can be sure of its being case-insensitive. This is recommended. |
|---|
| 514 | |
|---|
| 515 | =cut |
|---|
| 516 | |
|---|
| 517 | sub list_recent_changes { |
|---|
| 518 | my ($self, @args) = @_; |
|---|
| 519 | $self->store->list_recent_changes( @args ); |
|---|
| 520 | } |
|---|
| 521 | |
|---|
| 522 | =item B<list_unmoderated_nodes> |
|---|
| 523 | |
|---|
| 524 | my @nodes = $wiki->list_unmoderated_nodes(); |
|---|
| 525 | my @nodes = $wiki->list_unmoderated_nodes( |
|---|
| 526 | only_where_latest => 1 |
|---|
| 527 | ); |
|---|
| 528 | |
|---|
| 529 | $nodes[0]->{'name'} # The name of the node |
|---|
| 530 | $nodes[0]->{'node_id'} # The id of the node |
|---|
| 531 | $nodes[0]->{'version'} # The version in need of moderation |
|---|
| 532 | $nodes[0]->{'moderated_version'} # The newest moderated version |
|---|
| 533 | |
|---|
| 534 | Fetches details of all the node versions that require moderation (id, |
|---|
| 535 | name, version, and latest moderated version). |
|---|
| 536 | |
|---|
| 537 | If only_where_latest is set, then only the latest version of nodes where |
|---|
| 538 | the latest version needs moderating are returned. |
|---|
| 539 | Otherwise, all node versions (including old ones, and possibly multiple |
|---|
| 540 | per node) are returned. |
|---|
| 541 | =cut |
|---|
| 542 | |
|---|
| 543 | sub list_unmoderated_nodes { |
|---|
| 544 | my ($self, @args) = @_; |
|---|
| 545 | $self->store->list_unmoderated_nodes( @args ); |
|---|
| 546 | } |
|---|
| 547 | |
|---|
| 548 | =item B<list_node_all_versions> |
|---|
| 549 | |
|---|
| 550 | my @versions = $wiki->list_node_all_versions("HomePage"); |
|---|
| 551 | |
|---|
| 552 | my @versions = $wiki->list_node_all_versions( |
|---|
| 553 | name => 'HomePage', |
|---|
| 554 | with_content => 1, |
|---|
| 555 | with_metadata => 0 |
|---|
| 556 | ); |
|---|
| 557 | |
|---|
| 558 | Returns all the versions of a node, optionally including the content |
|---|
| 559 | and metadata, as an array of hashes (newest versions first). |
|---|
| 560 | =cut |
|---|
| 561 | sub list_node_all_versions { |
|---|
| 562 | my ($self,@argsarray) = @_; |
|---|
| 563 | |
|---|
| 564 | my %args; |
|---|
| 565 | if(scalar @argsarray == 1) { |
|---|
| 566 | $args{'name'} = $argsarray[0]; |
|---|
| 567 | } else { |
|---|
| 568 | %args = @argsarray; |
|---|
| 569 | } |
|---|
| 570 | |
|---|
| 571 | return $self->store->list_node_all_versions(%args); |
|---|
| 572 | } |
|---|
| 573 | |
|---|
| 574 | =item B<list_last_version_before> |
|---|
| 575 | List the last version of every node before a given date. |
|---|
| 576 | If no version existed before that date, will return undef for version. |
|---|
| 577 | Returns a hash of id, name, version and date |
|---|
| 578 | |
|---|
| 579 | my @nv = $wiki->list_last_version_before('2007-01-02 10:34:11') |
|---|
| 580 | foreach my $data (@nv) { |
|---|
| 581 | |
|---|
| 582 | } |
|---|
| 583 | =cut |
|---|
| 584 | sub list_last_version_before { |
|---|
| 585 | my ($self,@argsarray) = @_; |
|---|
| 586 | |
|---|
| 587 | return $self->store->list_last_version_before(@argsarray); |
|---|
| 588 | } |
|---|
| 589 | |
|---|
| 590 | =item B<node_exists> |
|---|
| 591 | |
|---|
| 592 | my $ok = $wiki->node_exists( "Wombat Defenestration" ); |
|---|
| 593 | |
|---|
| 594 | # or ignore case - optional but recommended |
|---|
| 595 | my $ok = $wiki->node_exists( |
|---|
| 596 | name => "monkey brains", |
|---|
| 597 | ignore_case => 1, |
|---|
| 598 | ); |
|---|
| 599 | |
|---|
| 600 | Returns true if the node has ever been created (even if it is |
|---|
| 601 | currently empty), and false otherwise. |
|---|
| 602 | |
|---|
| 603 | By default, the case-sensitivity of C<node_exists> depends on your |
|---|
| 604 | store backend. If you supply a true value to the C<ignore_case> |
|---|
| 605 | parameter, then you can be sure of its being case-insensitive. This |
|---|
| 606 | is recommended. |
|---|
| 607 | |
|---|
| 608 | =cut |
|---|
| 609 | |
|---|
| 610 | sub node_exists { |
|---|
| 611 | my ($self, @args) = @_; |
|---|
| 612 | $self->store->node_exists( @args ); |
|---|
| 613 | } |
|---|
| 614 | |
|---|
| 615 | =item B<node_required_moderation> |
|---|
| 616 | |
|---|
| 617 | my $needs = $wiki->node_required_moderation( "Wombat Defenestration" ); |
|---|
| 618 | |
|---|
| 619 | Returns true if the node exists and requires moderation, and false otherwise. |
|---|
| 620 | |
|---|
| 621 | =cut |
|---|
| 622 | |
|---|
| 623 | sub node_required_moderation { |
|---|
| 624 | my ($self, @args) = @_; |
|---|
| 625 | my %node = $self->retrieve_node(@args); |
|---|
| 626 | |
|---|
| 627 | # Return false if it doesn't exist |
|---|
| 628 | unless(%node) { return 0; } |
|---|
| 629 | unless($node{node_requires_moderation}) { return 0; } |
|---|
| 630 | |
|---|
| 631 | # Otherwise return the state of the flag |
|---|
| 632 | return $node{node_requires_moderation}; |
|---|
| 633 | } |
|---|
| 634 | |
|---|
| 635 | =item B<delete_node> |
|---|
| 636 | |
|---|
| 637 | $wiki->delete_node( name => "Home Page", version => 15 ); |
|---|
| 638 | |
|---|
| 639 | C<version> is optional. If it is supplied then only that version of |
|---|
| 640 | the node will be deleted. Otherwise the node and all its history will |
|---|
| 641 | be completely deleted. |
|---|
| 642 | |
|---|
| 643 | Doesn't do any locking though - to fix? You probably don't want to let |
|---|
| 644 | anyone except Wiki admins call this. You may not want to use it at |
|---|
| 645 | all. |
|---|
| 646 | |
|---|
| 647 | Croaks on error, silently does nothing if the node or version doesn't |
|---|
| 648 | exist, returns true if no error. |
|---|
| 649 | |
|---|
| 650 | =cut |
|---|
| 651 | |
|---|
| 652 | sub delete_node { |
|---|
| 653 | my $self = shift; |
|---|
| 654 | # Backwards compatibility. |
|---|
| 655 | my %args = ( scalar @_ == 1 ) ? ( name => $_[0] ) : @_; |
|---|
| 656 | |
|---|
| 657 | my @plugins = $self->get_registered_plugins; |
|---|
| 658 | my $plugins_ref = \@plugins if scalar @plugins; |
|---|
| 659 | |
|---|
| 660 | return 1 unless $self->node_exists( $args{name} ); |
|---|
| 661 | $self->store->delete_node( |
|---|
| 662 | name => $args{name}, |
|---|
| 663 | version => $args{version}, |
|---|
| 664 | wiki => $self, |
|---|
| 665 | plugins => $plugins_ref, |
|---|
| 666 | ); |
|---|
| 667 | |
|---|
| 668 | if ( my $search = $self->search_obj ) { |
|---|
| 669 | # Remove old data. |
|---|
| 670 | $search->delete_node( $args{name} ); |
|---|
| 671 | # If we have any versions left, index the new latest version. |
|---|
| 672 | my $new_current_content = $self->retrieve_node( $args{name } ); |
|---|
| 673 | if ( $new_current_content ) { |
|---|
| 674 | $search->index_node( $args{name}, $new_current_content ); |
|---|
| 675 | } |
|---|
| 676 | } |
|---|
| 677 | |
|---|
| 678 | return 1; |
|---|
| 679 | } |
|---|
| 680 | |
|---|
| 681 | =item B<search_nodes> |
|---|
| 682 | |
|---|
| 683 | # Find all the nodes which contain the word 'expert'. |
|---|
| 684 | my %results = $wiki->search_nodes('expert'); |
|---|
| 685 | |
|---|
| 686 | Returns a (possibly empty) hash whose keys are the node names and |
|---|
| 687 | whose values are the scores in some kind of relevance-scoring system I |
|---|
| 688 | haven't entirely come up with yet. For OR searches, this could |
|---|
| 689 | initially be the number of terms that appear in the node, perhaps. |
|---|
| 690 | |
|---|
| 691 | Defaults to AND searches (if $and_or is not supplied, or is anything |
|---|
| 692 | other than C<OR> or C<or>). |
|---|
| 693 | |
|---|
| 694 | Searches are case-insensitive. |
|---|
| 695 | |
|---|
| 696 | Croaks if you haven't defined a search backend. |
|---|
| 697 | |
|---|
| 698 | =cut |
|---|
| 699 | |
|---|
| 700 | sub search_nodes { |
|---|
| 701 | my ($self, @args) = @_; |
|---|
| 702 | my @terms = map { $self->store->charset_encode($_) } @args; |
|---|
| 703 | if ( $self->search_obj ) { |
|---|
| 704 | $self->search_obj->search_nodes( @terms ); |
|---|
| 705 | } else { |
|---|
| 706 | croak "No search backend defined."; |
|---|
| 707 | } |
|---|
| 708 | } |
|---|
| 709 | |
|---|
| 710 | =item B<supports_phrase_searches> |
|---|
| 711 | |
|---|
| 712 | if ( $wiki->supports_phrase_searches ) { |
|---|
| 713 | return $wiki->search_nodes( '"fox in socks"' ); |
|---|
| 714 | } |
|---|
| 715 | |
|---|
| 716 | Returns true if your chosen search backend supports phrase searching, |
|---|
| 717 | and false otherwise. |
|---|
| 718 | |
|---|
| 719 | =cut |
|---|
| 720 | |
|---|
| 721 | sub supports_phrase_searches { |
|---|
| 722 | my ($self, @args) = @_; |
|---|
| 723 | $self->search_obj->supports_phrase_searches( @args ) if $self->search_obj; |
|---|
| 724 | } |
|---|
| 725 | |
|---|
| 726 | =item B<supports_fuzzy_searches> |
|---|
| 727 | |
|---|
| 728 | if ( $wiki->supports_fuzzy_searches ) { |
|---|
| 729 | return $wiki->fuzzy_title_match( 'Kings Cross, St Pancreas' ); |
|---|
| 730 | } |
|---|
| 731 | |
|---|
| 732 | Returns true if your chosen search backend supports fuzzy title searching, |
|---|
| 733 | and false otherwise. |
|---|
| 734 | |
|---|
| 735 | =cut |
|---|
| 736 | |
|---|
| 737 | sub supports_fuzzy_searches { |
|---|
| 738 | my ($self, @args) = @_; |
|---|
| 739 | $self->search_obj->supports_fuzzy_searches( @args ) if $self->search_obj; |
|---|
| 740 | } |
|---|
| 741 | |
|---|
| 742 | =item B<fuzzy_title_match> |
|---|
| 743 | |
|---|
| 744 | B<NOTE:> This section of the documentation assumes you are using a |
|---|
| 745 | search engine which supports fuzzy matching. (See above.) The |
|---|
| 746 | L<Wiki::Toolkit::Search::DBIxFTS> backend in particular does not. |
|---|
| 747 | |
|---|
| 748 | $wiki->write_node( "King's Cross St Pancras", "A station." ); |
|---|
| 749 | my %matches = $wiki->fuzzy_title_match( "Kings Cross St. Pancras" ); |
|---|
| 750 | |
|---|
| 751 | Returns a (possibly empty) hash whose keys are the node names and |
|---|
| 752 | whose values are the scores in some kind of relevance-scoring system I |
|---|
| 753 | haven't entirely come up with yet. |
|---|
| 754 | |
|---|
| 755 | Note that even if an exact match is found, any other similar enough |
|---|
| 756 | matches will also be returned. However, any exact match is guaranteed |
|---|
| 757 | to have the highest relevance score. |
|---|
| 758 | |
|---|
| 759 | The matching is done against "canonicalised" forms of the search |
|---|
| 760 | string and the node titles in the database: stripping vowels, repeated |
|---|
| 761 | letters and non-word characters, and lowercasing. |
|---|
| 762 | |
|---|
| 763 | Croaks if you haven't defined a search backend. |
|---|
| 764 | |
|---|
| 765 | =cut |
|---|
| 766 | |
|---|
| 767 | sub fuzzy_title_match { |
|---|
| 768 | my ($self, @args) = @_; |
|---|
| 769 | if ( $self->search_obj ) { |
|---|
| 770 | if ($self->search_obj->supports_fuzzy_searches) { |
|---|
| 771 | $self->search_obj->fuzzy_title_match( @args ); |
|---|
| 772 | } else { |
|---|
| 773 | croak "Search backend doesn't support fuzzy searches"; |
|---|
| 774 | } |
|---|
| 775 | } else { |
|---|
| 776 | croak "No search backend defined."; |
|---|
| 777 | } |
|---|
| 778 | } |
|---|
| 779 | |
|---|
| 780 | =item B<register_plugin> |
|---|
| 781 | |
|---|
| 782 | my $plugin = Wiki::Toolkit::Plugin::Foo->new; |
|---|
| 783 | $wiki->register_plugin( plugin => $plugin ); |
|---|
| 784 | |
|---|
| 785 | Registers the plugin with the wiki as one that needs to be informed |
|---|
| 786 | when we write a node. |
|---|
| 787 | |
|---|
| 788 | If the plugin C<isa> L<Wiki::Toolkit::Plugin>, calls the methods set up by |
|---|
| 789 | that parent class to let it know about the backend store, search and |
|---|
| 790 | formatter objects. |
|---|
| 791 | |
|---|
| 792 | Finally, calls the plugin class's C<on_register> method, which should |
|---|
| 793 | be used to check tables are set up etc. Note that because of the order |
|---|
| 794 | these things are done in, C<on_register> for L<Wiki::Toolkit::Plugin> |
|---|
| 795 | subclasses can use the C<datastore>, C<indexer> and C<formatter> |
|---|
| 796 | methods as it needs to. |
|---|
| 797 | |
|---|
| 798 | =cut |
|---|
| 799 | |
|---|
| 800 | sub register_plugin { |
|---|
| 801 | my ($self, %args) = @_; |
|---|
| 802 | my $plugin = $args{plugin} || ""; |
|---|
| 803 | croak "no plugin supplied" unless $plugin; |
|---|
| 804 | if ( $plugin->isa( "Wiki::Toolkit::Plugin" ) ) { |
|---|
| 805 | $plugin->wiki( $self ); |
|---|
| 806 | $plugin->datastore( $self->store ); |
|---|
| 807 | $plugin->indexer( $self->search_obj ); |
|---|
| 808 | $plugin->formatter( $self->formatter ); |
|---|
| 809 | } |
|---|
| 810 | if ( $plugin->can( "on_register" ) ) { |
|---|
| 811 | $plugin->on_register; |
|---|
| 812 | } |
|---|
| 813 | push @{ $self->{_registered_plugins} }, $plugin; |
|---|
| 814 | } |
|---|
| 815 | |
|---|
| 816 | =item B<get_registered_plugins> |
|---|
| 817 | |
|---|
| 818 | my @plugins = $wiki->get_registered_plugins; |
|---|
| 819 | |
|---|
| 820 | Returns an array of plugin objects. |
|---|
| 821 | |
|---|
| 822 | =cut |
|---|
| 823 | |
|---|
| 824 | sub get_registered_plugins { |
|---|
| 825 | my $self = shift; |
|---|
| 826 | my $ref = $self->{_registered_plugins}; |
|---|
| 827 | return wantarray ? @$ref : $ref; |
|---|
| 828 | } |
|---|
| 829 | |
|---|
| 830 | =item B<write_node> |
|---|
| 831 | |
|---|
| 832 | my $written = $wiki->write_node($node, $content, $checksum, \%metadata, $requires_moderation); |
|---|
| 833 | if ($written) { |
|---|
| 834 | display_node($node); |
|---|
| 835 | } else { |
|---|
| 836 | handle_conflict(); |
|---|
| 837 | } |
|---|
| 838 | |
|---|
| 839 | Writes the specified content into the specified node in the backend |
|---|
| 840 | storage; and indexes/reindexes the node in the search indexes (if a |
|---|
| 841 | search is set up); calls C<post_write> on any registered plugins. |
|---|
| 842 | |
|---|
| 843 | Note that you can blank out a node without deleting it by passing the |
|---|
| 844 | empty string as $content, if you want to. |
|---|
| 845 | |
|---|
| 846 | If you expect the node to already exist, you must supply a checksum, |
|---|
| 847 | and the node is write-locked until either your checksum has been |
|---|
| 848 | proved old, or your checksum has been accepted and your change |
|---|
| 849 | committed. If no checksum is supplied, and the node is found to |
|---|
| 850 | already exist and be nonempty, a conflict will be raised. |
|---|
| 851 | |
|---|
| 852 | The first two parameters are mandatory, the others optional. If you |
|---|
| 853 | want to supply metadata but have no checksum (for a newly-created |
|---|
| 854 | node), supply a checksum of C<undef>. |
|---|
| 855 | |
|---|
| 856 | The final parameter, $requires_moderation (which defaults to false), |
|---|
| 857 | is ignored except on new nodes. For existing nodes, use |
|---|
| 858 | $wiki->toggle_node_moderation to change the node moderation flag. |
|---|
| 859 | |
|---|
| 860 | Returns 1 on success, 0 on conflict, croaks on error. |
|---|
| 861 | |
|---|
| 862 | B<Note> on the metadata hashref: Any data in here that you wish to |
|---|
| 863 | access directly later must be a key-value pair in which the value is |
|---|
| 864 | either a scalar or a reference to an array of scalars. For example: |
|---|
| 865 | |
|---|
| 866 | $wiki->write_node( "Calthorpe Arms", "nice pub", $checksum, |
|---|
| 867 | { category => [ "Pubs", "Bloomsbury" ], |
|---|
| 868 | postcode => "WC1X 8JR" } ); |
|---|
| 869 | |
|---|
| 870 | # and later |
|---|
| 871 | |
|---|
| 872 | my @nodes = $wiki->list_nodes_by_metadata( |
|---|
| 873 | metadata_type => "category", |
|---|
| 874 | metadata_value => "Pubs" ); |
|---|
| 875 | |
|---|
| 876 | For more advanced usage (passing data through to registered plugins) |
|---|
| 877 | you may if you wish pass key-value pairs in which the value is a |
|---|
| 878 | hashref or an array of hashrefs. The data in the hashrefs will not be |
|---|
| 879 | stored as metadata; it will be checksummed and the checksum will be |
|---|
| 880 | stored instead. Such data can I<only> be accessed via plugins. |
|---|
| 881 | |
|---|
| 882 | =cut |
|---|
| 883 | |
|---|
| 884 | sub write_node { |
|---|
| 885 | my ($self, $node, $content, $checksum, $metadata, $requires_moderation) = @_; |
|---|
| 886 | croak "No valid node name supplied for writing" unless $node; |
|---|
| 887 | croak "No content parameter supplied for writing" unless defined $content; |
|---|
| 888 | $checksum = md5_hex("") unless defined $checksum; |
|---|
| 889 | |
|---|
| 890 | my $formatter = $self->{_formatter}; |
|---|
| 891 | |
|---|
| 892 | my @links_to; |
|---|
| 893 | if ( $formatter->can( "find_internal_links" ) ) { |
|---|
| 894 | # Supply $metadata to formatter in case it's needed to alter the |
|---|
| 895 | # behaviour of the formatter, eg for Wiki::Toolkit::Formatter::Multiple. |
|---|
| 896 | my @all_links_to = $formatter->find_internal_links($content,$metadata); |
|---|
| 897 | my %unique = map { $_ => 1 } @all_links_to; |
|---|
| 898 | @links_to = keys %unique; |
|---|
| 899 | } |
|---|
| 900 | |
|---|
| 901 | my %data = ( node => $node, |
|---|
| 902 | content => $content, |
|---|
| 903 | checksum => $checksum, |
|---|
| 904 | metadata => $metadata, |
|---|
| 905 | requires_moderation => $requires_moderation ); |
|---|
| 906 | $data{links_to} = \@links_to if scalar @links_to; |
|---|
| 907 | my @plugins = $self->get_registered_plugins; |
|---|
| 908 | $data{plugins} = \@plugins if scalar @plugins; |
|---|
| 909 | |
|---|
| 910 | my $store = $self->store; |
|---|
| 911 | my $ret = $store->check_and_write_node( %data ) or return 0; |
|---|
| 912 | if($ret == -1) { return -1; } |
|---|
| 913 | |
|---|
| 914 | my $search = $self->{_search}; |
|---|
| 915 | if ($search and $content) { |
|---|
| 916 | $search->index_node($node, $store->charset_encode($content) ); |
|---|
| 917 | } |
|---|
| 918 | return 1; |
|---|
| 919 | } |
|---|
| 920 | |
|---|
| 921 | =item B<format> |
|---|
| 922 | |
|---|
| 923 | my $cooked = $wiki->format($raw, $metadata); |
|---|
| 924 | |
|---|
| 925 | Passed straight through to your chosen formatter object. You do not |
|---|
| 926 | I<have> to supply the C<$metadata> hashref, but if your formatter |
|---|
| 927 | allows node metadata to affect the rendering of the node then you |
|---|
| 928 | will want to. |
|---|
| 929 | |
|---|
| 930 | =cut |
|---|
| 931 | |
|---|
| 932 | sub format { |
|---|
| 933 | my ( $self, $raw, $metadata ) = @_; |
|---|
| 934 | my $formatter = $self->{_formatter}; |
|---|
| 935 | # Add on $self to the call so the formatter can access things like whether |
|---|
| 936 | # a linked-to node exists, etc. |
|---|
| 937 | my $result = $formatter->format( $raw, $self, $metadata ); |
|---|
| 938 | |
|---|
| 939 | # Nasty hack to work around an HTML::Parser deficiency |
|---|
| 940 | # see http://rt.cpan.org/NoAuth/Bug.html?id=7014 |
|---|
| 941 | if ($CAN_USE_ENCODE) { |
|---|
| 942 | if (Encode::is_utf8($raw)) { |
|---|
| 943 | Encode::_utf8_on( $result ); |
|---|
| 944 | } |
|---|
| 945 | } |
|---|
| 946 | |
|---|
| 947 | return $result; |
|---|
| 948 | } |
|---|
| 949 | |
|---|
| 950 | =item B<store> |
|---|
| 951 | |
|---|
| 952 | my $store = $wiki->store; |
|---|
| 953 | my $dbname = eval { $wiki->store->dbname; } |
|---|
| 954 | or warn "Not a DB backend"; |
|---|
| 955 | |
|---|
| 956 | Returns the storage backend object. |
|---|
| 957 | |
|---|
| 958 | =cut |
|---|
| 959 | |
|---|
| 960 | sub store { |
|---|
| 961 | my $self = shift; |
|---|
| 962 | return $self->{_store}; |
|---|
| 963 | } |
|---|
| 964 | |
|---|
| 965 | =item B<search_obj> |
|---|
| 966 | |
|---|
| 967 | my $search_obj = $wiki->search_obj; |
|---|
| 968 | |
|---|
| 969 | Returns the search backend object. |
|---|
| 970 | |
|---|
| 971 | =cut |
|---|
| 972 | |
|---|
| 973 | sub search_obj { |
|---|
| 974 | my $self = shift; |
|---|
| 975 | return $self->{_search}; |
|---|
| 976 | } |
|---|
| 977 | |
|---|
| 978 | =item B<formatter> |
|---|
| 979 | |
|---|
| 980 | my $formatter = $wiki->formatter; |
|---|
| 981 | |
|---|
| 982 | Returns the formatter backend object. |
|---|
| 983 | |
|---|
| 984 | =cut |
|---|
| 985 | |
|---|
| 986 | sub formatter { |
|---|
| 987 | my $self = shift; |
|---|
| 988 | return $self->{_formatter}; |
|---|
| 989 | } |
|---|
| 990 | |
|---|
| 991 | =head1 SEE ALSO |
|---|
| 992 | |
|---|
| 993 | For a very quick Wiki startup without any of that icky programming |
|---|
| 994 | stuff, see Tom Insam's L<Wiki::Toolkit::Kwiki>, an instant wiki based on |
|---|
| 995 | Wiki::Toolkit. |
|---|
| 996 | |
|---|
| 997 | Or for the specialised application of a wiki about a city, see the |
|---|
| 998 | L<OpenGuides> distribution. |
|---|
| 999 | |
|---|
| 1000 | L<Wiki::Toolkit> allows you to use different formatting modules. |
|---|
| 1001 | L<Text::WikiFormat> might be useful for anyone wanting to write a |
|---|
| 1002 | custom formatter. Existing formatters include: |
|---|
| 1003 | |
|---|
| 1004 | =over 4 |
|---|
| 1005 | |
|---|
| 1006 | =item * L<Wiki::Toolkit::Formatter::Default> (in this distro) |
|---|
| 1007 | |
|---|
| 1008 | =item * L<Wiki::Toolkit::Formatter::Pod> |
|---|
| 1009 | |
|---|
| 1010 | =item * L<Wiki::Toolkit::Formatter::UseMod> |
|---|
| 1011 | |
|---|
| 1012 | =back |
|---|
| 1013 | |
|---|
| 1014 | There's currently a choice of three storage backends - all |
|---|
| 1015 | database-backed. |
|---|
| 1016 | |
|---|
| 1017 | =over 4 |
|---|
| 1018 | |
|---|
| 1019 | =item * L<Wiki::Toolkit::Store::MySQL> (in this distro) |
|---|
| 1020 | |
|---|
| 1021 | =item * L<Wiki::Toolkit::Store::Pg> (in this distro) |
|---|
| 1022 | |
|---|
| 1023 | =item * L<Wiki::Toolkit::Store::SQLite> (in this distro) |
|---|
| 1024 | |
|---|
| 1025 | =item * L<Wiki::Toolkit::Store::Database> (parent class for the above - in this distro) |
|---|
| 1026 | |
|---|
| 1027 | =back |
|---|
| 1028 | |
|---|
| 1029 | A search backend is optional: |
|---|
| 1030 | |
|---|
| 1031 | =over 4 |
|---|
| 1032 | |
|---|
| 1033 | =item * L<Wiki::Toolkit::Search::DBIxFTS> (in this distro, uses L<DBIx::FullTextSearch>) |
|---|
| 1034 | |
|---|
| 1035 | =item * L<Wiki::Toolkit::Search::SII> (in this distro, uses L<Search::InvertedIndex>) |
|---|
| 1036 | |
|---|
| 1037 | =back |
|---|
| 1038 | |
|---|
| 1039 | Standalone plugins can also be written - currently they should only |
|---|
| 1040 | read from the backend storage, but write access guidelines are coming |
|---|
| 1041 | soon. Plugins written so far and available from CPAN: |
|---|
| 1042 | |
|---|
| 1043 | =over 4 |
|---|
| 1044 | |
|---|
| 1045 | =item * L<Wiki::Toolkit::Plugin::GeoCache> |
|---|
| 1046 | |
|---|
| 1047 | =item * L<Wiki::Toolkit::Plugin::Categoriser> |
|---|
| 1048 | |
|---|
| 1049 | =item * L<Wiki::Toolkit::Plugin::Locator::UK> |
|---|
| 1050 | |
|---|
| 1051 | =item * L<Wiki::Toolkit::Plugin::RSS::ModWiki> |
|---|
| 1052 | |
|---|
| 1053 | =back |
|---|
| 1054 | |
|---|
| 1055 | If writing a plugin you might want an easy way to run tests for it on |
|---|
| 1056 | all possible backends: |
|---|
| 1057 | |
|---|
| 1058 | =over 4 |
|---|
| 1059 | |
|---|
| 1060 | =item * L<Wiki::Toolkit::TestConfig::Utilities> (in this distro) |
|---|
| 1061 | |
|---|
| 1062 | =back |
|---|
| 1063 | |
|---|
| 1064 | Other ways to implement Wikis in Perl include: |
|---|
| 1065 | |
|---|
| 1066 | =over 4 |
|---|
| 1067 | |
|---|
| 1068 | =item * L<CGI::Kwiki> (an instant wiki) |
|---|
| 1069 | |
|---|
| 1070 | =item * L<CGI::pWiki> |
|---|
| 1071 | |
|---|
| 1072 | =item * L<AxKit::XSP::Wiki> |
|---|
| 1073 | |
|---|
| 1074 | =item * L<Apache::MiniWiki> |
|---|
| 1075 | |
|---|
| 1076 | =item * UseModWiki L<http://usemod.com> |
|---|
| 1077 | |
|---|
| 1078 | =item * Chiq Chaq L<http://chiqchaq.sourceforge.net/> |
|---|
| 1079 | |
|---|
| 1080 | =back |
|---|
| 1081 | |
|---|
| 1082 | =head1 AUTHOR |
|---|
| 1083 | |
|---|
| 1084 | Kake Pugh (kake@earth.li) and the Wiki::Toolkit team (including Nick Burch |
|---|
| 1085 | and Dominic Hargreaves) |
|---|
| 1086 | |
|---|
| 1087 | =head1 SUPPORT |
|---|
| 1088 | |
|---|
| 1089 | Questions should go to cgi-wiki-dev@earth.li. |
|---|
| 1090 | |
|---|
| 1091 | =head1 COPYRIGHT |
|---|
| 1092 | |
|---|
| 1093 | Copyright (C) 2002-2004 Kake Pugh. All Rights Reserved. |
|---|
| 1094 | Copyright (C) 2006 the Wiki::Toolkit team. All Rights Reserved. |
|---|
| 1095 | |
|---|
| 1096 | This module is free software; you can redistribute it and/or modify it |
|---|
| 1097 | under the same terms as Perl itself. |
|---|
| 1098 | |
|---|
| 1099 | =head1 FEEDBACK |
|---|
| 1100 | |
|---|
| 1101 | The developer web site and bug tracker is at |
|---|
| 1102 | http://www.wiki-toolkit.org/ - please file bugs there as appropriate. |
|---|
| 1103 | |
|---|
| 1104 | You could also subscribe to the dev list at |
|---|
| 1105 | http://www.earth.li/cgi-bin/mailman/listinfo/cgi-wiki-dev |
|---|
| 1106 | |
|---|
| 1107 | =head1 CREDITS |
|---|
| 1108 | |
|---|
| 1109 | Various London.pm types helped out with code review, encouragement, |
|---|
| 1110 | JFDI, style advice, code snippets, module recommendations, and so on; |
|---|
| 1111 | far too many to name individually, but particularly Richard Clamp, |
|---|
| 1112 | Tony Fisher, Mark Fowler, and Chris Ball. |
|---|
| 1113 | |
|---|
| 1114 | blair christensen sent patches and gave me some good ideas. chromatic |
|---|
| 1115 | continues to patiently apply my patches to L<Text::WikiFormat> and |
|---|
| 1116 | help me get it working in just the way I need. Paul Makepeace helped |
|---|
| 1117 | me add support for connecting to non-local databases. Shevek has been |
|---|
| 1118 | prodding me a lot lately. The L<OpenGuides> team keep me well-supplied |
|---|
| 1119 | with encouragement and bug reports. |
|---|
| 1120 | |
|---|
| 1121 | Nick Burch has been leading the way with development leading up to the |
|---|
| 1122 | release under the Wiki::Toolkit name. |
|---|
| 1123 | |
|---|
| 1124 | =head1 GRATUITOUS PLUG |
|---|
| 1125 | |
|---|
| 1126 | I'm only obsessed with Wikis because of the Open Guide to London -- |
|---|
| 1127 | L<http://openguides.org/london/> |
|---|
| 1128 | |
|---|
| 1129 | =cut |
|---|
| 1130 | |
|---|
| 1131 | 1; |
|---|