source: trunk/lib/OpenGuides.pm @ 831

Last change on this file since 831 was 831, checked in by nick, 15 years ago

Tweak display of un-moderated pages, and listings of them

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 51.3 KB
Line 
1package OpenGuides;
2use strict;
3
4use Carp "croak";
5use CGI;
6use Wiki::Toolkit::Plugin::Diff;
7use Wiki::Toolkit::Plugin::Locator::Grid;
8use OpenGuides::CGI;
9use OpenGuides::Feed;
10use OpenGuides::Template;
11use OpenGuides::Utils;
12use Time::Piece;
13use URI::Escape;
14
15use vars qw( $VERSION );
16
17$VERSION = '0.56';
18
19=head1 NAME
20
21OpenGuides - A complete web application for managing a collaboratively-written guide to a city or town.
22
23=head1 DESCRIPTION
24
25The OpenGuides software provides the framework for a collaboratively-written
26city guide.  It is similar to a wiki but provides somewhat more structured
27data storage allowing you to annotate wiki pages with information such as
28category, location, and much more.  It provides searching facilities
29including "find me everything within a certain distance of this place".
30Every page includes a link to a machine-readable (RDF) version of the page.
31
32=head1 METHODS
33
34=over
35
36=item B<new>
37
38  my $config = OpenGuides::Config->new( file => "wiki.conf" );
39  my $guide = OpenGuides->new( config => $config );
40
41=cut
42
43sub new {
44    my ($class, %args) = @_;
45    my $self = {};
46    bless $self, $class;
47    my $wiki = OpenGuides::Utils->make_wiki_object( config => $args{config} );
48    $self->{wiki} = $wiki;
49    $self->{config} = $args{config};
50    my $geo_handler = $self->config->geo_handler;
51    my $locator;
52    if ( $geo_handler == 1 ) {
53        $locator = Wiki::Toolkit::Plugin::Locator::Grid->new(
54                                             x => "os_x",    y => "os_y" );
55    } elsif ( $geo_handler == 2 ) {
56        $locator = Wiki::Toolkit::Plugin::Locator::Grid->new(
57                                             x => "osie_x",  y => "osie_y" );
58    } else {
59        $locator = Wiki::Toolkit::Plugin::Locator::Grid->new(
60                                             x => "easting", y => "northing" );
61    }
62    $wiki->register_plugin( plugin => $locator );
63    $self->{locator} = $locator;
64    my $differ = Wiki::Toolkit::Plugin::Diff->new;
65    $wiki->register_plugin( plugin => $differ );
66    $self->{differ} = $differ;
67    return $self;
68}
69
70=item B<wiki>
71
72An accessor, returns the underlying L<Wiki::Toolkit> object.
73
74=cut
75
76sub wiki {
77    my $self = shift;
78    return $self->{wiki};
79}
80
81=item B<config>
82
83An accessor, returns the underlying L<OpenGuides::Config> object.
84
85=cut
86
87sub config {
88    my $self = shift;
89    return $self->{config};
90}
91
92=item B<locator>
93
94An accessor, returns the underlying L<Wiki::Toolkit::Plugin::Locator::UK> object.
95
96=cut
97
98sub locator {
99    my $self = shift;
100    return $self->{locator};
101}
102
103=item B<differ>
104
105An accessor, returns the underlying L<Wiki::Toolkit::Plugin::Diff> object.
106
107=cut
108
109sub differ {
110    my $self = shift;
111    return $self->{differ};
112}
113
114=item B<display_node>
115
116  # Print node to STDOUT.
117  $guide->display_node(
118                          id      => "Calthorpe Arms",
119                          version => 2,
120                      );
121
122  # Or return output as a string (useful for writing tests).
123  $guide->display_node(
124                          id            => "Calthorpe Arms",
125                          return_output => 1,
126                      );
127
128  # Or return the hash of variables that will be passed to the template
129  # (not including those set additionally by OpenGuides::Template).
130  $guide->display_node(
131                          id             => "Calthorpe Arms",
132                          return_tt_vars => 1,
133                      );
134
135If C<version> is omitted then the latest version will be displayed.
136
137=cut
138
139sub display_node {
140    my ($self, %args) = @_;
141    my $return_output = $args{return_output} || 0;
142    my $version = $args{version};
143    my $id = $args{id} || $self->config->home_name;
144    my $wiki = $self->wiki;
145    my $config = $self->config;
146    my $oldid = $args{oldid} || '';
147    my $do_redirect = $args{redirect} || 1;
148
149    my %tt_vars;
150
151    if ( $id =~ /^(Category|Locale) (.*)$/ ) {
152        my $type = $1;
153        $tt_vars{is_indexable_node} = 1;
154        $tt_vars{index_type} = lc($type);
155        $tt_vars{index_value} = $2;
156        $tt_vars{"rss_".lc($type)."_url"} =
157                           $config->script_name . "?action=rc;format=rss;"
158                           . lc($type) . "=" . lc(CGI->escape($2));
159        $tt_vars{"atom_".lc($type)."_url"} =
160                           $config->script_name . "?action=rc;format=atom;"
161                           . lc($type) . "=" . lc(CGI->escape($2));
162    }
163
164    my %current_data = $wiki->retrieve_node( $id );
165    my $current_version = $current_data{version};
166    undef $version if ($version && $version == $current_version);
167    my %criteria = ( name => $id );
168    $criteria{version} = $version if $version; # retrieve_node default is current
169
170    my %node_data = $wiki->retrieve_node( %criteria );
171
172    # Fixes passing undefined values to Text::Wikiformat if node doesn't exist.
173    my $raw        = $node_data{content} || " ";
174    my $content    = $wiki->format($raw);
175    my $modified   = $node_data{last_modified};
176    my $moderated  = $node_data{moderated};
177    my %metadata   = %{$node_data{metadata}};
178
179    my ($wgs84_long, $wgs84_lat) = OpenGuides::Utils->get_wgs84_coords(
180                                        longitude => $metadata{longitude}[0],
181                                        latitude => $metadata{latitude}[0],
182                                        config => $config);
183    if ($args{format} && $args{format} eq 'raw') {
184      print "Content-Type: text/plain\n\n";
185      print $raw;
186      return 0;
187    }
188   
189    my %metadata_vars = OpenGuides::Template->extract_metadata_vars(
190                            wiki     => $wiki,
191                            config   => $config,
192                            metadata => $node_data{metadata}
193                        );
194
195    %tt_vars = (
196                   %tt_vars,
197                   %metadata_vars,
198                   content       => $content,
199                   last_modified => $modified,
200                   version       => $node_data{version},
201                   node          => $id,
202                   language      => $config->default_language,
203                   moderated     => $moderated,
204                   oldid         => $oldid,
205                   enable_gmaps  => 1,
206                   display_google_maps => $self->get_cookie("display_google_maps"),
207                   wgs84_long    => $wgs84_long,
208                   wgs84_lat     => $wgs84_lat
209               );
210
211    if ( $raw =~ /^#REDIRECT\s+(.+?)\s*$/ ) {
212        my $redirect = $1;
213        # Strip off enclosing [[ ]] in case this is an extended link.
214        $redirect =~ s/^\[\[//;
215        $redirect =~ s/\]\]\s*$//;
216
217        # Don't redirect if the parameter "redirect" is given as 0.
218        if ($do_redirect == 0) {
219            return %tt_vars if $args{return_tt_vars};
220            $tt_vars{current} = 1;
221            my $output = $self->process_template(
222                                                  id            => $id,
223                                                  template      => "node.tt",
224                                                  tt_vars       => \%tt_vars,
225                                                );
226            return $output if $return_output;
227            print $output;
228        } elsif ( $wiki->node_exists($redirect) && $redirect ne $id && $redirect ne $oldid ) {
229            # Avoid loops by not generating redirects to the same node or the previous node.
230            my $output = $self->redirect_to_node($redirect, $id);
231            return $output if $return_output;
232            print $output;
233            return 0;
234        }
235    }
236
237    # We've undef'ed $version above if this is the current version.
238    $tt_vars{current} = 1 unless $version;
239
240    if ($id eq "RecentChanges") {
241        $self->display_recent_changes(%args);
242    } elsif ( $id eq $self->config->home_name ) {
243        my @recent = $wiki->list_recent_changes(
244            last_n_changes => 10,
245            metadata_was   => { edit_type => "Normal edit" },
246        );
247        @recent = map {
248                          {
249                              name          => CGI->escapeHTML($_->{name}),
250                              last_modified => CGI->escapeHTML($_->{last_modified}),
251                              version       => CGI->escapeHTML($_->{version}),
252                              comment       => CGI->escapeHTML($_->{metadata}{comment}[0]),
253                              username      => CGI->escapeHTML($_->{metadata}{username}[0]),
254                              url           => $config->script_name . "?"
255                                               . CGI->escape($wiki->formatter->node_name_to_node_param($_->{name}))
256                          }
257                      } @recent;
258        $tt_vars{recent_changes} = \@recent;
259        return %tt_vars if $args{return_tt_vars};
260        my $output = $self->process_template(
261                                                id            => $id,
262                                                template      => "home_node.tt",
263                                                tt_vars       => \%tt_vars,
264                                            );
265        return $output if $return_output;
266        print $output;
267    } else {
268        return %tt_vars if $args{return_tt_vars};
269        my $output = $self->process_template(
270                                                id            => $id,
271                                                template      => "node.tt",
272                                                tt_vars       => \%tt_vars,
273                                            );
274        return $output if $return_output;
275        print $output;
276    }
277}
278
279=item B<display_recent_changes> 
280
281  $guide->display_recent_changes;
282
283As with other methods, the C<return_output> parameter can be used to
284return the output instead of printing it to STDOUT.
285
286=cut
287
288sub display_recent_changes {
289    my ($self, %args) = @_;
290    my $config = $self->config;
291    my $wiki = $self->wiki;
292    my $minor_edits = $self->get_cookie( "show_minor_edits_in_rc" );
293    my $id = $args{id} || $self->config->home_name;
294    my $return_output = $args{return_output} || 0;
295    my (%tt_vars, %recent_changes);
296    my $q = CGI->new;
297    my $since = $q->param("since");
298    if ( $since ) {
299        $tt_vars{since} = $since;
300        my $t = localtime($since); # overloaded by Time::Piece
301        $tt_vars{since_string} = $t->strftime;
302        my %criteria = ( since => $since );   
303        $criteria{metadata_was} = { edit_type => "Normal edit" }
304          unless $minor_edits;
305        my @rc = $self->{wiki}->list_recent_changes( %criteria );
306 
307        @rc = map {
308            {
309              name        => CGI->escapeHTML($_->{name}),
310              last_modified => CGI->escapeHTML($_->{last_modified}),
311              version     => CGI->escapeHTML($_->{version}),
312              comment     => CGI->escapeHTML($_->{metadata}{comment}[0]),
313              username    => CGI->escapeHTML($_->{metadata}{username}[0]),
314              host        => CGI->escapeHTML($_->{metadata}{host}[0]),
315              username_param => CGI->escape($_->{metadata}{username}[0]),
316              edit_type   => CGI->escapeHTML($_->{metadata}{edit_type}[0]),
317              url         => $config->script_name . "?"
318      . CGI->escape($wiki->formatter->node_name_to_node_param($_->{name})),
319        }
320                   } @rc;
321        if ( scalar @rc ) {
322            $recent_changes{since} = \@rc; 
323        }
324    } else {
325        for my $days ( [0, 1], [1, 7], [7, 14], [14, 30] ) {
326            my %criteria = ( between_days => $days );
327            $criteria{metadata_was} = { edit_type => "Normal edit" }
328              unless $minor_edits;
329            my @rc = $self->{wiki}->list_recent_changes( %criteria );
330
331            @rc = map {
332            {
333              name        => CGI->escapeHTML($_->{name}),
334              last_modified => CGI->escapeHTML($_->{last_modified}),
335              version     => CGI->escapeHTML($_->{version}),
336              comment     => CGI->escapeHTML($_->{metadata}{comment}[0]),
337              username    => CGI->escapeHTML($_->{metadata}{username}[0]),
338              host        => CGI->escapeHTML($_->{metadata}{host}[0]),
339              username_param => CGI->escape($_->{metadata}{username}[0]),
340              edit_type   => CGI->escapeHTML($_->{metadata}{edit_type}[0]),
341              url         => $config->script_name . "?"
342      . CGI->escape($wiki->formatter->node_name_to_node_param($_->{name})),
343        }
344                       } @rc;
345            if ( scalar @rc ) {
346                $recent_changes{$days->[1]} = \@rc;
347        }
348        }
349    }
350    $tt_vars{recent_changes} = \%recent_changes;
351    my %processing_args = (
352                            id            => $id,
353                            template      => "recent_changes.tt",
354                            tt_vars       => \%tt_vars,
355                           );
356    if ( !$since && $self->get_cookie("track_recent_changes_views") ) {
357    my $cookie =
358           OpenGuides::CGI->make_recent_changes_cookie(config => $config );
359        $processing_args{cookies} = $cookie;
360        $tt_vars{last_viewed} = OpenGuides::CGI->get_last_recent_changes_visit_from_cookie( config => $config );
361    }
362    return %tt_vars if $args{return_tt_vars};
363    my $output = $self->process_template( %processing_args );
364    return $output if $return_output;
365    print $output;
366}
367
368=item B<display_diffs>
369
370  $guide->display_diffs(
371                           id            => "Home Page",
372                           version       => 6,
373                           other_version => 5,
374                       );
375
376  # Or return output as a string (useful for writing tests).
377  my $output = $guide->display_diffs(
378                                        id            => "Home Page",
379                                        version       => 6,
380                                        other_version => 5,
381                                        return_output => 1,
382                                    );
383
384  # Or return the hash of variables that will be passed to the template
385  # (not including those set additionally by OpenGuides::Template).
386  my %vars = $guide->display_diffs(
387                                      id             => "Home Page",
388                                      version        => 6,
389                                      other_version  => 5,
390                                      return_tt_vars => 1,
391                                  );
392
393=cut
394
395sub display_diffs {
396    my ($self, %args) = @_;
397    my %diff_vars = $self->differ->differences(
398                                                  node          => $args{id},
399                                                  left_version  => $args{version},
400                                                  right_version => $args{other_version},
401                                              );
402    $diff_vars{not_deletable} = 1;
403    $diff_vars{not_editable}  = 1;
404    $diff_vars{deter_robots}  = 1;
405    return %diff_vars if $args{return_tt_vars};
406    my $output = $self->process_template(
407                                            id       => $args{id},
408                                            template => "differences.tt",
409                                            tt_vars  => \%diff_vars
410                                        );
411    return $output if $args{return_output};
412    print $output;
413}
414
415=item B<find_within_distance>
416
417  $guide->find_within_distance(
418                                  id => $node,
419                                  metres => $q->param("distance_in_metres")
420                              );
421
422=cut
423
424sub find_within_distance {
425    my ($self, %args) = @_;
426    my $node = $args{id};
427    my $metres = $args{metres};
428    my %data = $self->wiki->retrieve_node( $node );
429    my $lat = $data{metadata}{latitude}[0];
430    my $long = $data{metadata}{longitude}[0];
431    my $script_url = $self->config->script_url;
432    my $q = CGI->new;
433    print $q->redirect( $script_url . "search.cgi?lat=$lat;long=$long;distance_in_metres=$metres" );
434}
435
436=item B<show_backlinks>
437
438  $guide->show_backlinks( id => "Calthorpe Arms" );
439
440As with other methods, parameters C<return_tt_vars> and
441C<return_output> can be used to return these things instead of
442printing the output to STDOUT.
443
444=cut
445
446sub show_backlinks {
447    my ($self, %args) = @_;
448    my $wiki = $self->wiki;
449    my $formatter = $wiki->formatter;
450
451    my @backlinks = $wiki->list_backlinks( node => $args{id} );
452    my @results = map {
453                          {
454                              url   => CGI->escape($formatter->node_name_to_node_param($_)),
455                              title => CGI->escapeHTML($_)
456                          }
457                      } sort @backlinks;
458    my %tt_vars = ( results       => \@results,
459                    num_results   => scalar @results,
460                    not_deletable => 1,
461                    deter_robots  => 1,
462                    not_editable  => 1 );
463    return %tt_vars if $args{return_tt_vars};
464    my $output = OpenGuides::Template->output(
465                                                 node    => $args{id},
466                                                 wiki    => $wiki,
467                                                 config  => $self->config,
468                                                 template=>"backlink_results.tt",
469                                                 vars    => \%tt_vars,
470                                             );
471    return $output if $args{return_output};
472    print $output;
473}
474
475=item B<show_index>
476
477  $guide->show_index(
478                        type   => "category",
479                        value  => "pubs",
480                    );
481
482  # RDF version.
483  $guide->show_index(
484                        type   => "locale",
485                        value  => "Holborn",
486                        format => "rdf",
487                    );
488
489  # RSS / Atom version (recent changes style).
490  $guide->show_index(
491                        type   => "locale",
492                        value  => "Holborn",
493                        format => "rss",
494                    );
495
496  # Or return output as a string (useful for writing tests).
497  $guide->show_index(
498                        type          => "category",
499                        value         => "pubs",
500                        return_output => 1,
501                    );
502
503=cut
504
505sub show_index {
506    my ($self, %args) = @_;
507    my $wiki = $self->wiki;
508    my $formatter = $wiki->formatter;
509    my %tt_vars;
510    my @selnodes;
511
512    if ( $args{type} and $args{value} ) {
513        if ( $args{type} eq "fuzzy_title_match" ) {
514            my %finds = $wiki->fuzzy_title_match( $args{value} );
515            @selnodes = sort { $finds{$a} <=> $finds{$b} } keys %finds;
516            $tt_vars{criterion} = {
517                type  => $args{type},  # for RDF version
518                value => $args{value}, # for RDF version
519                name  => CGI->escapeHTML("Fuzzy Title Match on '$args{value}'")
520            };
521            $tt_vars{not_editable} = 1;
522        } else {
523            @selnodes = $wiki->list_nodes_by_metadata(
524                metadata_type  => $args{type},
525                metadata_value => $args{value},
526                ignore_case    => 1
527            );
528            my $name = ucfirst($args{type}) . " $args{value}";
529            my $url = $self->config->script_name
530                      . "?"
531                      . ucfirst( $args{type} )
532                      . "_"
533                      . uri_escape(
534                                      $formatter->node_name_to_node_param($args{value})
535                                  );
536            $tt_vars{criterion} = {
537                type  => $args{type},
538                value => $args{value}, # for RDF version
539                name  => CGI->escapeHTML( $name ),
540                url   => $url
541            };
542            $tt_vars{not_editable} = 1;
543        }
544    } else {
545        @selnodes = $wiki->list_all_nodes();
546    }
547
548    my @nodes = map {
549                        {
550                            name      => $_,
551                            node_data => { $wiki->retrieve_node( name => $_ ) },
552                            param     => $formatter->node_name_to_node_param($_) }
553                        } sort @selnodes;
554
555    $tt_vars{nodes} = \@nodes;
556
557    my ($template, %conf);
558
559    if ( $args{format} ) {
560        if ( $args{format} eq "rdf" ) {
561            $template = "rdf_index.tt";
562            $conf{content_type} = "application/rdf+xml";
563        }
564        elsif ( $args{format} eq "plain" ) {
565            $template = "plain_index.tt";
566            $conf{content_type} = "text/plain";
567        } elsif ( $args{format} eq "map" ) {
568            my $q = CGI->new;
569            $tt_vars{zoom} = $q->param('zoom') || '';
570            $tt_vars{lat} = $q->param('lat') || '';
571            $tt_vars{long} = $q->param('long') || '';
572            $tt_vars{centre_long} = $self->config->centre_long;
573            $tt_vars{centre_lat} = $self->config->centre_lat;
574            $tt_vars{default_gmaps_zoom} = $self->config->default_gmaps_zoom;
575            $tt_vars{enable_gmaps} = 1;
576            $tt_vars{display_google_maps} = 1; # override for this page
577            $template = "map_index.tt";
578           
579        } elsif( $args{format} eq "rss" || $args{format} eq "atom") {
580            # They really wanted a recent changes style rss/atom feed
581            my $feed_type = $args{format};
582            my ($feed,$content_type) = $self->get_feed_and_content_type($feed_type);
583            $feed->set_feed_name_and_url_params(
584                        "Index of $args{type} $args{value}",
585                        "action=index;index_type=$args{type};index_value=$args{value}"
586            );
587
588            # Grab the actual node data out of @nodes
589            my @node_data;
590            foreach my $node (@nodes) {
591                $node->{node_data}->{name} = $node->{name};
592                push @node_data, $node->{node_data};
593            }
594
595            my $output = "Content-Type: ".$content_type."\n";
596            $output .= $feed->build_feed_for_nodes($feed_type, @node_data);
597
598            return $output if $args{return_output};
599            print $output;
600            return;
601        }
602    } else {
603        $template = "site_index.tt";
604    }
605
606    %conf = (
607                %conf,
608                node        => "$args{type} index", # KLUDGE
609                template    => $template,
610                tt_vars     => \%tt_vars,
611            );
612
613    my $output = $self->process_template( %conf );
614    return $output if $args{return_output};
615    print $output;
616}
617
618=item B<list_all_versions>
619
620  $guide->list_all_versions ( id => "Home Page" );
621
622  # Or return output as a string (useful for writing tests).
623  $guide->list_all_versions (
624                                id            => "Home Page",
625                                return_output => 1,
626                            );
627
628  # Or return the hash of variables that will be passed to the template
629  # (not including those set additionally by OpenGuides::Template).
630  $guide->list_all_versions (
631                                id             => "Home Page",
632                                return_tt_vars => 1,
633                            );
634
635=cut
636
637sub list_all_versions {
638    my ($self, %args) = @_;
639    my $return_output = $args{return_output} || 0;
640    my $node = $args{id};
641    my %curr_data = $self->wiki->retrieve_node($node);
642    my $curr_version = $curr_data{version};
643    my @history;
644    for my $version ( 1 .. $curr_version ) {
645        my %node_data = $self->wiki->retrieve_node( name    => $node,
646                                                    version => $version );
647        # $node_data{version} will be zero if this version was deleted.
648        push @history, {
649            version  => CGI->escapeHTML( $version ),
650            modified => CGI->escapeHTML( $node_data{last_modified} ),
651            username => CGI->escapeHTML( $node_data{metadata}{username}[0] ),
652            comment  => CGI->escapeHTML( $node_data{metadata}{comment}[0] ),
653                       } if $node_data{version};
654    }
655    @history = reverse @history;
656    my %tt_vars = (
657                      node          => $node,
658                      version       => $curr_version,
659                      not_deletable => 1,
660                      not_editable  => 1,
661                      deter_robots  => 1,
662                      history       => \@history
663                  );
664    return %tt_vars if $args{return_tt_vars};
665    my $output = $self->process_template(
666                                            id       => $node,
667                                            template => "node_history.tt",
668                                            tt_vars  => \%tt_vars,
669                                        );
670    return $output if $return_output;
671    print $output;
672}
673
674=item B<get_feed_and_content_type>
675
676Fetch the OpenGuides feed object, and the output content type, for the
677supplied feed type.
678
679Handles all the setup for the OpenGuides feed object.
680=cut
681sub get_feed_and_content_type {
682    my ($self, $feed_type) = @_;
683
684    my $feed = OpenGuides::Feed->new(
685                                        wiki       => $self->wiki,
686                                        config     => $self->config,
687                                        og_version => $VERSION,
688                                    );
689
690    my $content_type = $feed->default_content_type($feed_type);
691
692    return ($feed, $content_type);
693}
694
695=item B<display_feed>
696
697  # Last ten non-minor edits to Hammersmith pages in RSS 1.0 format
698  $guide->display_feed(
699                         feed_type          => 'rss',
700                         feed_listing       => 'recent_changes',
701                         items              => 10,
702                         ignore_minor_edits => 1,
703                         locale             => "Hammersmith",
704                     );
705
706  # All edits bob has made to pub pages in the last week in Atom format
707  $guide->display_feed(
708                         feed_type    => 'atom',
709                         feed_listing => 'recent_changes',
710                         days         => 7,
711                         username     => "bob",
712                         category     => "Pubs",
713                     );
714
715C<feed_type> is a mandatory parameter. Supported values at present are
716"rss" and "atom".
717
718C<feed_listing> is a mandatory parameter. Supported values at present
719are "recent_changes". (More values are coming soon though!)
720
721As with other methods, the C<return_output> parameter can be used to
722return the output instead of printing it to STDOUT.
723
724=cut
725
726sub display_feed {
727    my ($self, %args) = @_;
728
729    my $feed_type = $args{feed_type};
730    croak "No feed type given" unless $feed_type;
731
732    my $feed_listing = $args{feed_listing};
733    croak "No feed listing given" unless $feed_listing;
734   
735    my $return_output = $args{return_output} ? 1 : 0;
736
737    # Basic criteria, whatever the feed listing type is
738    my %criteria = (
739                       feed_type             => $feed_type,
740                       feed_listing          => $feed_listing,
741                       also_return_timestamp => 1,
742                   );
743
744    # Feed listing specific criteria
745    if($feed_listing eq "recent_changes") {
746        $criteria{items} = $args{items} || "";
747        $criteria{days}  = $args{days}  || "";
748        $criteria{ignore_minor_edits} = $args{ignore_minor_edits} ? 1 : 0;
749
750        my $username = $args{username} || "";
751        my $category = $args{category} || "";
752        my $locale   = $args{locale}   || "";
753
754        my %filter;
755        $filter{username} = $username if $username;
756        $filter{category} = $category if $category;
757        $filter{locale}   = $locale   if $locale;
758        if ( scalar keys %filter ) {
759            $criteria{filter_on_metadata} = \%filter;
760        }
761    }
762    elsif($feed_listing eq "node_all_versions") {
763        $criteria{name} = $args{name};
764    }
765
766
767    # Get the feed object, and the content type
768    my ($feed,$content_type) = $self->get_feed_and_content_type($feed_type);
769
770    my $output = "Content-Type: ".$content_type."\n";
771   
772    # Get the feed, and the timestamp, in one go
773    my ($feed_output, $feed_timestamp) = 
774        $feed->make_feed( %criteria );
775
776    $output .= "Last-Modified: " . $feed_timestamp . "\n\n";
777    $output .= $feed_output;
778
779    return $output if $return_output;
780    print $output;
781}
782
783sub display_about {
784    my ($self, %args) = @_;
785
786    my $output;
787
788    if ($args{format} && $args{format} =~ /^rdf$/i) {
789        $output = qq{Content-Type: application/rdf+xml
790
791<?xml version="1.0" encoding="UTF-8"?>
792<rdf:RDF xmlns      = "http://usefulinc.com/ns/doap#"
793         xmlns:rdf  = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
794         xmlns:foaf = "http://xmlns.com/foaf/0.1/">
795<Project rdf:ID="OpenGuides">
796  <name>OpenGuides</name>
797
798  <created>2003-04-29</created>
799 
800  <shortdesc xml:lang="en">
801    A wiki engine for collaborative description of places with specialised
802    geodata metadata features.
803  </shortdesc>
804
805  <description xml:lang="en">
806    OpenGuides is a collaborative wiki environment, written in Perl, for
807    building guides and sharing information, as both human-readable text
808    and RDF. The engine contains a number of geodata-specific metadata
809    mechanisms such as locale search, node classification and integration
810    with Google Maps.
811  </description>
812
813  <homepage rdf:resource="http://openguides.org/" />
814  <mailing-list rdf:resource="http://openguides.org/mm/listinfo/openguides-dev/" />
815  <mailing-list rdf:resource="http://urchin.earth.li/mailman/listinfo/openguides-commits/" />
816
817  <maintainer>
818    <foaf:Person rdf:ID="OpenGuidesMaintainer">
819      <foaf:name>Dominic Hargreaves</foaf:name>
820      <foaf:homepage rdf:resource="http://www.larted.org.uk/~dom/" />
821    </foaf:Person>
822  </maintainer>
823
824  <repository>
825    <SVNRepository rdf:ID="OpenGuidesSVN">
826      <location rdf:resource="https://urchin.earth.li/svn/openguides/" />
827      <browse rdf:resource="http://dev.openguides.org/browser" />
828    </SVNRepository>
829  </repository>
830
831  <release>
832    <Version rdf:ID="OpenGuidesVersion">
833      <revision>$VERSION</revision>
834    </Version>
835  </release>
836
837  <download-page rdf:resource="http://search.cpan.org/dist/OpenGuides/" />
838 
839  <!-- Freshmeat category: Internet :: WWW/HTTP :: Dynamic Content -->
840  <category rdf:resource="http://freshmeat.net/browse/92/" />
841 
842  <license rdf:resource="http://www.opensource.org/licenses/gpl-license.php" />
843  <license rdf:resource="http://www.opensource.org/licenses/artistic-license.php" />
844
845</Project>
846
847</rdf:RDF>};
848    }
849    else {
850        my $site_name  = $self->config->{site_name};
851        my $script_name = $self->config->{script_name};
852        $output = qq{Content-Type: text/html; charset=utf-8
853
854<html>
855<head>
856  <title>About $site_name</title>
857<style type="text/css">
858body        { margin: 0px; }
859#content    { padding: 50px; margin: auto; width: 50%; }
860h1          { margin-bottom: 0px; font-style: italic; }
861h2          { margin-top: 0px; }
862#logo       { text-align: center; }
863#about      { margin: 0em 0em 1em 0em; border-top: 1px solid #ddd; border-bottom: 1px solid #ddd; }
864#meta       { font-size: small; text-align: center;}
865</style>
866<link rel="alternate"
867  type="application/rdf+xml"
868  title="DOAP (Description Of A Project) profile for this site's software"
869  href="$script_name?action=about;format=rdf" />
870</head>
871<body>
872<div id="content">
873<div id="logo">
874<a href="http://openguides.org/"><img
875src="http://openguides.org/img/logo.png" alt="OpenGuides"></a>
876<h1><a href="$script_name">$site_name</a></h1>
877<h2>is powered by <a href="http://openguides.org/">OpenGuides</a> -<br>
878the guides made by you.</h2>
879<h3>version <a href="http://search.cpan.org/~dom/OpenGuides-$VERSION">$VERSION</a></h3>
880</div>
881<div id="about">
882<p>
883<a href="http://www.w3.org/RDF/"><img
884src="http://openguides.org/img/rdf_icon.png" width="44" height="48"
885style="float: right; margin-left: 10px; border: 0px"></a> OpenGuides is a
886web-based collaborative <a href="http://wiki.org/wiki.cgi?WhatIsWiki">wiki</a>
887environment for building guides and sharing information, as both
888human-readable text and <a href="http://www.w3.org/RDF/"><acronym
889title="Resource Description Framework">RDF</acronym></a>. The engine contains
890a number of geodata-specific metadata mechanisms such as locale search, node
891classification and integration with <a href="http://maps.google.com/">Google
892Maps</a>.
893</p>
894<p>
895OpenGuides is written in <a href="http://www.perl.org/">Perl</a>, and is
896made available under the same license as Perl itself (dual <a
897href="http://dev.perl.org/licenses/artistic.html" title='The "Artistic Licence"'>Artistic</a> and <a
898href="http://www.opensource.org/licenses/gpl-license.php"><acronym
899title="GNU Public Licence">GPL</acronym></a>). Developer information for the
900project is available from the <a href="http://dev.openguides.org/">OpenGuides
901development site</a>.
902</p>
903<p>
904Copyright &copy;2003-2006, <a href="http://openguides.org/">The OpenGuides
905Project</a>. "OpenGuides", "[The] Open Guide To..." and "The guides made by
906you" are trademarks of The OpenGuides Project. Any uses on this site are made
907with permission.
908</p>
909</div>
910<div id="meta">
911<a href="$script_name?action=about;format=rdf"><acronym
912title="Description Of A Project">DOAP</acronym> RDF version of this
913information</a>
914</div>
915</div>
916</body>
917</html>};
918    }
919   
920    return $output if $args{return_output};
921    print $output;
922}
923
924=item B<commit_node>
925
926  $guide->commit_node(
927                         id      => $node,
928                         cgi_obj => $q,
929                     );
930
931As with other methods, parameters C<return_tt_vars> and
932C<return_output> can be used to return these things instead of
933printing the output to STDOUT.
934
935The geographical data that you should provide in the L<CGI> object
936depends on the handler you chose in C<wiki.conf>.
937
938=over
939
940=item *
941
942B<British National Grid> - provide either C<os_x> and C<os_y> or
943C<latitude> and C<longitude>; whichever set of data you give, it will
944be converted to the other and both sets will be stored.
945
946=item *
947
948B<Irish National Grid> - provide either C<osie_x> and C<osie_y> or
949C<latitude> and C<longitude>; whichever set of data you give, it will
950be converted to the other and both sets will be stored.
951
952=item *
953
954B<UTM ellipsoid> - provide C<latitude> and C<longitude>; these will be
955converted to easting and northing and both sets of data will be stored.
956
957=back
958
959=cut
960
961sub commit_node {
962    my ($self, %args) = @_;
963    my $node = $args{id};
964    my $q = $args{cgi_obj};
965    my $return_output = $args{return_output};
966    my $wiki = $self->wiki;
967    my $config = $self->config;
968
969    my $content  = $q->param("content");
970    $content =~ s/\r\n/\n/gs;
971    my $checksum = $q->param("checksum");
972
973    my %metadata = OpenGuides::Template->extract_metadata_vars(
974        wiki    => $wiki,
975        config  => $config,
976    cgi_obj => $q
977    );
978
979    delete $metadata{website} if $metadata{website} eq 'http://';
980
981    $metadata{opening_hours_text} = $q->param("hours_text") || "";
982
983    # Pick out the unmunged versions of lat/long if they're set.
984    # (If they're not, it means they weren't munged in the first place.)
985    $metadata{latitude} = delete $metadata{latitude_unmunged}
986        if $metadata{latitude_unmunged};
987    $metadata{longitude} = delete $metadata{longitude_unmunged}
988        if $metadata{longitude_unmunged};
989
990    # Check to make sure all the indexable nodes are created
991    # TODO: Split this off into another function
992    # TODO: Don't run this if the node requires moderation (only do it after someone moderates)
993    foreach my $type (qw(Category Locale)) {
994        my $lctype = lc($type);
995        foreach my $index (@{$metadata{$lctype}}) {
996            $index =~ s/(.*)/\u$1/;
997            my $node = $type . " " . $index;
998            # Uppercase the node name before checking for existence
999            $node =~ s/ (\S+)/ \u$1/g;
1000            unless ( $wiki->node_exists($node) ) {
1001                my $category = $type eq "Category" ? "Category" : "Locales";
1002                $wiki->write_node(
1003                                     $node,
1004                                     "\@INDEX_LINK [[$node]]",
1005                                     undef,
1006                                     {
1007                                         username => "Auto Create",
1008                                         comment  => "Auto created $lctype stub page",
1009                                         category => $category
1010                                     }
1011                                 );
1012            }
1013        }
1014    }
1015   
1016    foreach my $var ( qw( summary username comment edit_type ) ) {
1017        $metadata{$var} = $q->param($var) || "";
1018    }
1019    $metadata{host} = $ENV{REMOTE_ADDR};
1020
1021    # Wiki::Toolkit::Plugin::RSS::ModWiki wants "major_change" to be set.
1022    $metadata{major_change} = ( $metadata{edit_type} eq "Normal edit" )
1023                            ? 1
1024                            : 0;
1025
1026    my $written = $wiki->write_node($node, $content, $checksum, \%metadata );
1027
1028    if ($written) {
1029        my $output = $self->redirect_to_node($node);
1030        return $output if $return_output;
1031        print $output;
1032    } else {
1033        my %node_data = $wiki->retrieve_node($node);
1034        my %tt_vars = ( checksum       => $node_data{checksum},
1035                        new_content    => $content,
1036                        stored_content => $node_data{content} );
1037        foreach my $mdvar ( keys %metadata ) {
1038            if ($mdvar eq "locales") {
1039                $tt_vars{"stored_$mdvar"} = $node_data{metadata}{locale};
1040                $tt_vars{"new_$mdvar"}    = $metadata{locale};
1041            } elsif ($mdvar eq "categories") {
1042                $tt_vars{"stored_$mdvar"} = $node_data{metadata}{category};
1043                $tt_vars{"new_$mdvar"}    = $metadata{category};
1044            } elsif ($mdvar eq "username" or $mdvar eq "comment"
1045                      or $mdvar eq "edit_type" ) {
1046                $tt_vars{$mdvar} = $metadata{$mdvar};
1047            } else {
1048                $tt_vars{"stored_$mdvar"} = $node_data{metadata}{$mdvar}[0];
1049                $tt_vars{"new_$mdvar"}    = $metadata{$mdvar};
1050            }
1051        }
1052        return %tt_vars if $args{return_tt_vars};
1053        my $output = $self->process_template(
1054                                              id       => $node,
1055                                              template => "edit_conflict.tt",
1056                                              tt_vars  => \%tt_vars,
1057                                            );
1058        return $output if $args{return_output};
1059        print $output;
1060    }
1061}
1062
1063
1064=item B<delete_node>
1065
1066  $guide->delete_node(
1067                         id       => "FAQ",
1068                         version  => 15,
1069                         password => "beer",
1070                     );
1071
1072C<version> is optional - if it isn't supplied then all versions of the
1073node will be deleted; in other words the node will be entirely
1074removed.
1075
1076If C<password> is not supplied then a form for entering the password
1077will be displayed.
1078
1079As with other methods, parameters C<return_tt_vars> and
1080C<return_output> can be used to return these things instead of
1081printing the output to STDOUT.
1082
1083=cut
1084
1085sub delete_node {
1086    my ($self, %args) = @_;
1087    my $node = $args{id} or croak "No node ID supplied for deletion";
1088    my $return_tt_vars = $args{return_tt_vars} || 0;
1089    my $return_output = $args{return_output} || 0;
1090
1091    my %tt_vars = (
1092                      not_editable  => 1,
1093                      not_deletable => 1,
1094                      deter_robots  => 1,
1095                  );
1096    $tt_vars{delete_version} = $args{version} || "";
1097
1098    my $password = $args{password};
1099
1100    if ($password) {
1101        if ($password ne $self->config->admin_pass) {
1102            return %tt_vars if $return_tt_vars;
1103            my $output = $self->process_template(
1104                                                    id       => $node,
1105                                                    template => "delete_password_wrong.tt",
1106                                                    tt_vars  => \%tt_vars,
1107                                                );
1108            return $output if $return_output;
1109            print $output;
1110        } else {
1111            $self->wiki->delete_node(
1112                                        name    => $node,
1113                                        version => $args{version},
1114                                    );
1115            # Check whether any versions of this node remain.
1116            my %check = $self->wiki->retrieve_node( name => $node );
1117            $tt_vars{other_versions_remain} = 1 if $check{version};
1118            return %tt_vars if $return_tt_vars;
1119            my $output = $self->process_template(
1120                                                    id       => $node,
1121                                                    template => "delete_done.tt",
1122                                                    tt_vars  => \%tt_vars,
1123                                                );
1124            return $output if $return_output;
1125            print $output;
1126        }
1127    } else {
1128        return %tt_vars if $return_tt_vars;
1129        my $output = $self->process_template(
1130                                                id       => $node,
1131                                                template => "delete_confirm.tt",
1132                                                tt_vars  => \%tt_vars,
1133                                            );
1134        return $output if $return_output;
1135        print $output;
1136    }
1137}
1138
1139=item B<set_node_moderation>
1140
1141  $guide->set_node_moderation(
1142                         id       => "FAQ",
1143                         password => "beer",
1144                         moderation_flag => 1,
1145                     );
1146
1147Sets the moderation needed flag on a node, either on or off.
1148
1149If C<password> is not supplied then a form for entering the password
1150will be displayed.
1151=cut
1152sub set_node_moderation {
1153    my ($self, %args) = @_;
1154    my $node = $args{id} or croak "No node ID supplied for node moderation";
1155    my $return_tt_vars = $args{return_tt_vars} || 0;
1156    my $return_output = $args{return_output} || 0;
1157
1158    # Get the moderation flag into something sane
1159    if($args{moderation_flag} eq "1" || $args{moderation_flag} eq "yes" ||
1160       $args{moderation_flag} eq "on" || $args{moderation_flag} eq "true") {
1161        $args{moderation_flag} = 1;
1162    } else {
1163        $args{moderation_flag} = 0;
1164    }
1165
1166    # Set up the TT variables
1167    my %tt_vars = (
1168                      not_editable  => 1,
1169                      not_deletable => 1,
1170                      deter_robots  => 1,
1171                      moderation_action => 'set_moderation',
1172                      moderation_flag   => $args{moderation_flag},
1173                      moderation_url_args => 'action=set_moderation;moderation_flag='.$args{moderation_flag},
1174                  );
1175
1176    my $password = $args{password};
1177
1178    if ($password) {
1179        if ($password ne $self->config->admin_pass) {
1180            return %tt_vars if $return_tt_vars;
1181            my $output = $self->process_template(
1182                                                    id       => $node,
1183                                                    template => "moderate_password_wrong.tt",
1184                                                    tt_vars  => \%tt_vars,
1185                                                );
1186            return $output if $return_output;
1187            print $output;
1188        } else {
1189            $self->wiki->set_node_moderation(
1190                                        name    => $node,
1191                                        required => $args{moderation_flag},
1192                                    );
1193
1194            # Send back to the admin interface
1195            my $script_url = $self->config->script_url;
1196            my $script_name = $self->config->script_name;
1197            my $q = CGI->new;
1198            my $output = $q->redirect( $script_url.$script_name."?action=admin&moderation=changed" );
1199            return $output if $return_output;
1200            print $output;
1201        }
1202    } else {
1203        return %tt_vars if $return_tt_vars;
1204        my $output = $self->process_template(
1205                                                id       => $node,
1206                                                template => "moderate_confirm.tt",
1207                                                tt_vars  => \%tt_vars,
1208                                            );
1209        return $output if $return_output;
1210        print $output;
1211    }
1212}
1213
1214=item B<show_missing_metadata>
1215Search for nodes which don't have a certain kind of metadata. Optionally
1216also excludes Locales and Categories
1217=cut
1218sub show_missing_metadata {
1219    my ($self, %args) = @_;
1220    my $return_tt_vars = $args{return_tt_vars} || 0;
1221    my $return_output = $args{return_output} || 0;
1222
1223    my $wiki = $self->wiki;
1224    my $formatter = $self->wiki->formatter;
1225    my $script_url = $self->config->script_url;
1226
1227    my ($metadata_type, $metadata_value, $exclude_locales, $exclude_categories)
1228        = @args{ qw( metadata_type metadata_value exclude_locales exclude_categories ) };
1229
1230    my @nodes;
1231    my $done_search = 0;
1232
1233    # Only search if they supplied at least a metadata type
1234    if($metadata_type) {
1235        $done_search = 1;
1236        @nodes = $wiki->list_nodes_by_missing_metadata(
1237                            metadata_type => $metadata_type,
1238                            metadata_value => $metadata_value,
1239                            ignore_case    => 1,
1240        );
1241
1242        # Do we need to filter some nodes out?
1243        if($exclude_locales || $exclude_categories) {
1244            my @all_nodes = @nodes;
1245            @nodes = ();
1246
1247            foreach my $node (@all_nodes) {
1248                if($exclude_locales && $node =~ /^Locale /) { next; }
1249                if($exclude_categories && $node =~ /^Category /) { next; }
1250                push @nodes, $node;
1251            }
1252        }
1253    }
1254
1255    # Build nice edit etc links for our nodes
1256    my @tt_nodes;
1257    for my $node (@nodes) {
1258        my %n;
1259
1260        # Make the URLs
1261        my $node_param = uri_escape( $formatter->node_name_to_node_param( $node ) );
1262
1263        # Save into the hash
1264        $n{'name'} = $node;
1265        $n{'view_url'} = $script_url . "?id=" . $node_param;
1266        $n{'edit_url'} = $script_url . "?id=" . $node_param . ";action=edit";
1267        push @tt_nodes, \%n;
1268    }
1269
1270    # Set up our TT variables, including the search parameters
1271    my %tt_vars = (
1272                      not_editable  => 1,
1273                      not_deletable => 1,
1274                      deter_robots  => 1,
1275
1276                      nodes => \@tt_nodes,
1277                      done_search    => $done_search,
1278                      metadata_type  => $metadata_type,
1279                      metadata_value => $metadata_value,
1280                      exclude_locales => $exclude_locales,
1281                      exclude_categories => $exclude_categories
1282                  );
1283    return %tt_vars if $return_tt_vars;
1284
1285    # Render to the page
1286    my $output = $self->process_template(
1287                                           id       => "",
1288                                           template => "missing_metadata.tt",
1289                                           tt_vars  => \%tt_vars,
1290                                        );
1291    return $output if $return_output;
1292    print $output;
1293}
1294
1295=item B<display_admin_interface>
1296Fetch everything we need to display the admin interface, and passes it off
1297 to the template
1298=cut
1299sub display_admin_interface {
1300    my ($self, %args) = @_;
1301    my $return_tt_vars = $args{return_tt_vars} || 0;
1302    my $return_output = $args{return_output} || 0;
1303
1304    my $wiki = $self->wiki;
1305    my $formatter = $self->wiki->formatter;
1306    my $script_url = $self->config->script_url;
1307
1308    # Grab all the nodes
1309    my @all_nodes = $wiki->list_all_nodes(with_details=>1);
1310    @all_nodes = sort { $a->{'name'} cmp $b->{'name'} } @all_nodes;
1311
1312
1313    # Split into nodes, Locales and Categories
1314    my @nodes;
1315    my @categories;
1316    my @locales;
1317    for my $node (@all_nodes) {
1318        # Make the URLs
1319        my $node_param = uri_escape( $formatter->node_name_to_node_param( $node->{'name'} ) );
1320        $node->{'view_url'} = $script_url . "?id=" . $node_param;
1321        $node->{'versions_url'} = $script_url . "?action=list_all_versions;id=" . $node_param;
1322        $node->{'moderation_url'} = $script_url . "?action=set_moderation;id=" . $node_param;
1323
1324        # Filter
1325        if($node->{'name'} =~ /^Category /) {
1326            $node->{'page_name'} = $node->{'name'};
1327            $node->{'name'} =~ s/^Category //;
1328            push @categories, $node;
1329        } elsif($node->{'name'} =~ /^Locale /) {
1330            $node->{'page_name'} = $node->{'name'};
1331            $node->{'name'} =~ s/^Locale //;
1332            push @locales, $node;
1333        } else {
1334            push @nodes, $node;
1335        }
1336    }
1337
1338    # Render in a template
1339    my %tt_vars = (
1340                      not_editable  => 1,
1341                      not_deletable => 1,
1342                      deter_robots  => 1,
1343                      nodes => \@nodes,
1344                      categories => \@categories,
1345                      locales => \@locales
1346                  );
1347    return %tt_vars if $return_tt_vars;
1348    my $output = $self->process_template(
1349                                           id       => "",
1350                                           template => "admin_home.tt",
1351                                           tt_vars  => \%tt_vars,
1352                                        );
1353    return $output if $return_output;
1354    print $output;
1355}
1356
1357sub process_template {
1358    my ($self, %args) = @_;
1359    my %output_conf = (
1360                          wiki     => $self->wiki,
1361                          config   => $self->config,
1362                          node     => $args{id},
1363                          template => $args{template},
1364                          vars     => $args{tt_vars},
1365                          cookies  => $args{cookies},
1366                      );
1367    if ( $args{content_type} ) {
1368        $output_conf{content_type} = $args{content_type};
1369    }
1370    return OpenGuides::Template->output( %output_conf );
1371}
1372
1373sub redirect_to_node {
1374    my ($self, $node, $redirected_from) = @_;
1375   
1376    my $script_url = $self->config->script_url;
1377    my $script_name = $self->config->script_name;
1378    my $formatter = $self->wiki->formatter;
1379
1380    my $id = $formatter->node_name_to_node_param( $node );
1381    my $oldid;
1382    $oldid = $formatter->node_name_to_node_param( $redirected_from ) if $redirected_from;
1383
1384    my $redir_param = "$script_url$script_name?";
1385    $redir_param .= 'id=' if $oldid;
1386    $redir_param .= $id;
1387    $redir_param .= ";oldid=$oldid" if $oldid;
1388   
1389    my $q = CGI->new;
1390    return $q->redirect( $redir_param );
1391}
1392
1393sub get_cookie {
1394    my $self = shift;
1395    my $config = $self->config;
1396    my $pref_name = shift or return "";
1397    my %cookie_data = OpenGuides::CGI->get_prefs_from_cookie(config=>$config);
1398    return $cookie_data{$pref_name};
1399}
1400
1401
1402=head1 BUGS AND CAVEATS
1403
1404UTF8 data are currently not handled correctly throughout.
1405
1406Other bugs are documented at
1407L<http://dev.openguides.org/>
1408
1409=head1 SEE ALSO
1410
1411=over 4
1412
1413=item * L<http://london.openguides.org/|The Open Guide to London>, the first and biggest OpenGuides site.
1414
1415=item * L<http://openguides.org/|The OpenGuides website>, with a list of all live OpenGuides installs.
1416
1417=item * L<Wiki::Toolkit>, the Wiki toolkit which does the heavy lifting for OpenGuides
1418
1419=back
1420
1421=head1 FEEDBACK
1422
1423If you have a question, a bug report, or a patch, or you're interested
1424in joining the development team, please contact openguides-dev@openguides.org
1425(moderated mailing list, will reach all current developers but you'll have
1426to wait for your post to be approved) or file a bug report at
1427L<http://dev.openguides.org/>
1428
1429=head1 AUTHOR
1430
1431The OpenGuides Project (openguides-dev@openguides.org)
1432
1433=head1 COPYRIGHT
1434
1435     Copyright (C) 2003-2006 The OpenGuides Project.  All Rights Reserved.
1436
1437The OpenGuides distribution is free software; you can redistribute it
1438and/or modify it under the same terms as Perl itself.
1439
1440=head1 CREDITS
1441
1442Programming by Dominic Hargreaves, Earle Martin, Kake Pugh, and Ivor
1443Williams.  Testing and bug reporting by Billy Abbott, Jody Belka,
1444Kerry Bosworth, Simon Cozens, Cal Henderson, Steve Jolly, and Bob
1445Walker (among others).  Much of the Module::Build stuff copied from
1446the Siesta project L<http://siesta.unixbeard.net/>
1447
1448=cut
1449
14501;
Note: See TracBrowser for help on using the repository browser.