root/wiki-toolkit/trunk/lib/Wiki/Toolkit.pm @ 387

Revision 387, 33.2 KB (checked in by dom, 6 years ago)

Fix version number

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1package Wiki::Toolkit;
2
3use strict;
4
5use vars qw( $VERSION );
6$VERSION = '0.74';
7
8use Carp qw(croak carp);
9use 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
15my $CAN_USE_ENCODE;
16BEGIN {
17  eval " use Encode ";
18  $CAN_USE_ENCODE = $@ ? 0 : 1;
19}
20
21
22=head1 NAME
23
24Wiki::Toolkit - A toolkit for building Wikis.
25
26=head1 DESCRIPTION
27
28Helps you develop Wikis quickly by taking care of the boring bits for
29you.  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
98C<store> must be an object of type C<Wiki::Toolkit::Store::*> and
99C<search> if supplied must be of type C<Wiki::Toolkit::Search::*> (though
100this isn't checked yet - FIXME). If C<formatter> isn't supplied, it
101defaults to an object of class L<Wiki::Toolkit::Formatter::Default>.
102
103You can get a searchable Wiki up and running on a system without an
104actual database server by using the SQLite storage backend with the
105SII/DB_File search backend - cut and paste the lines above for a quick
106start, and see L<Wiki::Toolkit::Store::SQLite>, L<Wiki::Toolkit::Search::SII>,
107and L<Search::InvertedIndex::DB::DB_File_SplitHash> when you want to
108learn the details.
109
110C<formatter> can be any object that behaves in the right way; this
111essentially means that it needs to provide a C<format> method which
112takes in raw text and returns the formatted version. See
113L<Wiki::Toolkit::Formatter::Default> for a simple example. Note that you can
114create a suitable object from a sub very quickly by using
115L<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
122I'm not sure whether to put this in the module or not - it'd let you
123just supply a sub instead of an object as the formatter, but it feels
124wrong to be using a Test::* module in actual code.
125
126=cut
127
128sub new {
129    my ($class, @args) = @_;
130    my $self = {};
131    bless $self, $class;
132    $self->_init(@args) or return undef;
133    return $self;
134}
135
136sub _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
191In scalar context, returns the current (raw Wiki language) contents of
192the specified node. In list context, returns a hash containing the
193contents 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
204metadata sent along the last time the node was written
205
206=back
207
208The C<node> parameter is mandatory. The C<version> parameter is
209optional and defaults to the newest version. If the node hasn't been
210created yet, it is considered to exist but be empty (this behaviour
211might change).
212
213B<Note> on metadata - each hash value is returned as an array ref,
214even if that type of metadata only has one value.
215
216=cut
217
218sub 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
233Marks the given version of the node as moderated. If this is the
234highest moderated version, then update the node's contents to hold
235this version.
236=cut
237
238sub 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
252Sets if a node requires moderation or not.
253(Moderation is required when $required is true).
254
255When moderation is required, new versions of a node will sit about
256until they're tagged as moderated, when they will become the new node.
257=cut
258
259sub 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
268Renames a node, updating any references to it as required.
269
270Uses the internal_links table to identify the nodes that link to this
271one, and re-writes any wiki links in these to point to the new name. If
272required, it can mark these updates to other pages as a new version.
273=cut
274
275sub 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
301Sees whether your checksum is current for the given node. Returns true
302if so, false if not.
303
304B<NOTE:> Be aware that when called directly and without locking, this
305might not be accurate, since there is a small window between the
306checking and the returning where the node might be changed, so
307B<don't> rely on it for safe commits; use C<write_node> for that. It
308can however be useful when previewing edits, for example.
309
310=cut
311
312sub 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
324sub 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
335Each node is returned once only, regardless of how many other nodes
336link to it.
337
338=cut
339
340sub 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
349Returns a list containing the name of every existing node.  The list
350won't be in any kind of order; do any sorting in your calling script.
351
352=cut
353
354sub 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
379Returns a list containing the name of every node whose caller-supplied
380metadata matches the criteria given in the parameters.
381
382By default, the case-sensitivity of both C<metadata_type> and
383C<metadata_value> depends on your database - if it will return rows
384with an attribute value of "Pubs" when you asked for "pubs", or not.
385If you supply a true value to the C<ignore_case> parameter, then you
386can be sure of its being case-insensitive.  This is recommended.
387
388If you don't supply any criteria then you'll get an empty list.
389
390This is a really really really simple way of finding things; if you
391want to be more complicated then you'll need to call the method
392multiple times and combine the results yourself, or write a plugin.
393
394=cut
395
396sub 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>
402Returns nodes where either the metadata doesn't exist, or is blank
403   
404Unlike list_nodes_by_metadata(), the metadata value is optional (the
405metadata 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
419sub 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
473You I<must> supply one of the following constraints: C<days>
474(integer), C<since> (epoch), C<last_n_changes> (integer).
475
476You I<may> also supply I<either> C<metadata_is> (and optionally
477C<metadata_isnt>), I<or> C<metadata_was> (and optionally
478C<metadata_wasnt>). Each of these should be a ref to a hash with
479scalar keys and values.  If the hash has more than one entry, then
480only changes satisfying I<all> criteria will be returned when using
481C<metadata_is> or C<metadata_was>, but all changes which fail to
482satisfy any one of the criteria will be returned when using
483C<metadata_isnt> or C<metadata_is>.
484
485C<metadata_is> and C<metadata_isnt> look only at the metadata that the
486node I<currently> has. C<metadata_was> and C<metadata_wasnt> take into
487account the metadata of previous versions of a node.
488
489Returns results as an array, in reverse chronological order.  Each
490element 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
501to the current version of the node
502
503=back
504
505Unless you supply C<include_all_changes>, C<metadata_was> or
506C<metadata_wasnt>, each node will only be returned once regardless of
507how many times it has been changed recently.
508
509By default, the case-sensitivity of both C<metadata_type> and
510C<metadata_value> depends on your database - if it will return rows
511with an attribute value of "Pubs" when you asked for "pubs", or not.
512If you supply a true value to the C<ignore_case> parameter, then you
513can be sure of its being case-insensitive.  This is recommended.
514
515=cut
516
517sub 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
543sub 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
558Returns all the versions of a node, optionally including the content
559and metadata, as an array of hashes (newest versions first).
560=cut
561sub 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
584sub 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
600Returns true if the node has ever been created (even if it is
601currently empty), and false otherwise.
602
603By default, the case-sensitivity of C<node_exists> depends on your
604store backend.  If you supply a true value to the C<ignore_case>
605parameter, then you can be sure of its being case-insensitive.  This
606is recommended.
607
608=cut
609
610sub 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
619Returns true if the node exists and requires moderation, and false otherwise.
620
621=cut
622
623sub 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
639C<version> is optional.  If it is supplied then only that version of
640the node will be deleted.  Otherwise the node and all its history will
641be completely deleted.
642
643Doesn't do any locking though - to fix? You probably don't want to let
644anyone except Wiki admins call this. You may not want to use it at
645all.
646
647Croaks on error, silently does nothing if the node or version doesn't
648exist, returns true if no error.
649
650=cut
651
652sub 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
686Returns a (possibly empty) hash whose keys are the node names and
687whose values are the scores in some kind of relevance-scoring system I
688haven't entirely come up with yet. For OR searches, this could
689initially be the number of terms that appear in the node, perhaps.
690
691Defaults to AND searches (if $and_or is not supplied, or is anything
692other than C<OR> or C<or>).
693
694Searches are case-insensitive.
695
696Croaks if you haven't defined a search backend.
697
698=cut
699
700sub 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
716Returns true if your chosen search backend supports phrase searching,
717and false otherwise.
718
719=cut
720
721sub 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
732Returns true if your chosen search backend supports fuzzy title searching,
733and false otherwise.
734
735=cut
736
737sub 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
744B<NOTE:> This section of the documentation assumes you are using a
745search engine which supports fuzzy matching. (See above.) The
746L<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
751Returns a (possibly empty) hash whose keys are the node names and
752whose values are the scores in some kind of relevance-scoring system I
753haven't entirely come up with yet.
754
755Note that even if an exact match is found, any other similar enough
756matches will also be returned. However, any exact match is guaranteed
757to have the highest relevance score.
758
759The matching is done against "canonicalised" forms of the search
760string and the node titles in the database: stripping vowels, repeated
761letters and non-word characters, and lowercasing.
762
763Croaks if you haven't defined a search backend.
764
765=cut
766
767sub 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
785Registers the plugin with the wiki as one that needs to be informed
786when we write a node.
787
788If the plugin C<isa> L<Wiki::Toolkit::Plugin>, calls the methods set up by
789that parent class to let it know about the backend store, search and
790formatter objects.
791
792Finally, calls the plugin class's C<on_register> method, which should
793be used to check tables are set up etc. Note that because of the order
794these things are done in, C<on_register> for L<Wiki::Toolkit::Plugin>
795subclasses can use the C<datastore>, C<indexer> and C<formatter>
796methods as it needs to.
797
798=cut
799
800sub 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
820Returns an array of plugin objects.
821
822=cut
823
824sub 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
839Writes the specified content into the specified node in the backend
840storage; and indexes/reindexes the node in the search indexes (if a
841search is set up); calls C<post_write> on any registered plugins.
842
843Note that you can blank out a node without deleting it by passing the
844empty string as $content, if you want to.
845
846If you expect the node to already exist, you must supply a checksum,
847and the node is write-locked until either your checksum has been
848proved old, or your checksum has been accepted and your change
849committed.  If no checksum is supplied, and the node is found to
850already exist and be nonempty, a conflict will be raised.
851
852The first two parameters are mandatory, the others optional. If you
853want to supply metadata but have no checksum (for a newly-created
854node), supply a checksum of C<undef>.
855
856The final parameter, $requires_moderation (which defaults to false),
857is ignored except on new nodes. For existing nodes, use
858$wiki->toggle_node_moderation to change the node moderation flag.
859
860Returns 1 on success, 0 on conflict, croaks on error.
861
862B<Note> on the metadata hashref: Any data in here that you wish to
863access directly later must be a key-value pair in which the value is
864either 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
876For more advanced usage (passing data through to registered plugins)
877you may if you wish pass key-value pairs in which the value is a
878hashref or an array of hashrefs. The data in the hashrefs will not be
879stored as metadata; it will be checksummed and the checksum will be
880stored instead. Such data can I<only> be accessed via plugins.
881
882=cut
883
884sub 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
925Passed straight through to your chosen formatter object. You do not
926I<have> to supply the C<$metadata> hashref, but if your formatter
927allows node metadata to affect the rendering of the node then you
928will want to.
929
930=cut
931
932sub 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
956Returns the storage backend object.
957
958=cut
959
960sub 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
969Returns the search backend object.
970
971=cut
972
973sub search_obj {
974    my $self = shift;
975    return $self->{_search};
976}
977
978=item B<formatter>
979
980  my $formatter = $wiki->formatter;
981
982Returns the formatter backend object.
983
984=cut
985
986sub formatter {
987    my $self = shift;
988    return $self->{_formatter};
989}
990
991=head1 SEE ALSO
992
993For a very quick Wiki startup without any of that icky programming
994stuff, see Tom Insam's L<Wiki::Toolkit::Kwiki>, an instant wiki based on
995Wiki::Toolkit.
996
997Or for the specialised application of a wiki about a city, see the
998L<OpenGuides> distribution.
999
1000L<Wiki::Toolkit> allows you to use different formatting modules.
1001L<Text::WikiFormat> might be useful for anyone wanting to write a
1002custom 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
1014There's currently a choice of three storage backends - all
1015database-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
1029A 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
1039Standalone plugins can also be written - currently they should only
1040read from the backend storage, but write access guidelines are coming
1041soon. 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
1055If writing a plugin you might want an easy way to run tests for it on
1056all possible backends:
1057
1058=over 4
1059
1060=item * L<Wiki::Toolkit::TestConfig::Utilities> (in this distro)
1061
1062=back
1063
1064Other 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
1084Kake Pugh (kake@earth.li) and the Wiki::Toolkit team (including Nick Burch
1085and Dominic Hargreaves)
1086
1087=head1 SUPPORT
1088
1089Questions 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
1096This module is free software; you can redistribute it and/or modify it
1097under the same terms as Perl itself.
1098
1099=head1 FEEDBACK
1100
1101The developer web site and bug tracker is at
1102  http://www.wiki-toolkit.org/ - please file bugs there as appropriate.
1103
1104You 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
1109Various London.pm types helped out with code review, encouragement,
1110JFDI, style advice, code snippets, module recommendations, and so on;
1111far too many to name individually, but particularly Richard Clamp,
1112Tony Fisher, Mark Fowler, and Chris Ball.
1113
1114blair christensen sent patches and gave me some good ideas. chromatic
1115continues to patiently apply my patches to L<Text::WikiFormat> and
1116help me get it working in just the way I need. Paul Makepeace helped
1117me add support for connecting to non-local databases. Shevek has been
1118prodding me a lot lately. The L<OpenGuides> team keep me well-supplied
1119with encouragement and bug reports.
1120
1121Nick Burch has been leading the way with development leading up to the
1122release under the Wiki::Toolkit name.
1123
1124=head1 GRATUITOUS PLUG
1125
1126I'm only obsessed with Wikis because of the Open Guide to London --
1127L<http://openguides.org/london/>
1128
1129=cut
1130
11311;
Note: See TracBrowser for help on using the browser.