source: trunk/lib/OpenGuides.pm @ 544

Last change on this file since 544 was 544, checked in by kake, 17 years ago

Fix syntax error.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 25.6 KB
Line 
1package OpenGuides;
2use strict;
3
4use Carp "croak";
5use CGI;
6use CGI::Wiki::Plugin::Diff;
7use CGI::Wiki::Plugin::Locator::UK;
8use OpenGuides::CGI;
9use OpenGuides::Template;
10use OpenGuides::Utils;
11use Time::Piece;
12use URI::Escape;
13
14use vars qw( $VERSION );
15
16$VERSION = '0.43';
17
18=head1 NAME
19
20OpenGuides - A complete web application for managing a collaboratively-written guide to a city or town.
21
22=head1 DESCRIPTION
23
24The OpenGuides software provides the framework for a collaboratively-written
25city guide.  It is similar to a wiki but provides somewhat more structured
26data storage allowing you to annotate wiki pages with information such as
27category, location, and much more.  It provides searching facilities
28including "find me everything within a certain distance of this place".
29Every page includes a link to a machine-readable (RDF) version of the page.
30
31=head1 METHODS
32
33=over
34
35=item B<new>
36
37  my $guide = OpenGuides->new( config => $config );
38
39=cut
40
41sub new {
42    my ($class, %args) = @_;
43    my $self = {};
44    bless $self, $class;
45    my $wiki = OpenGuides::Utils->make_wiki_object( config => $args{config} );
46    $self->{wiki} = $wiki;
47    $self->{config} = $args{config};
48    my $locator = CGI::Wiki::Plugin::Locator::UK->new;
49    $wiki->register_plugin( plugin => $locator );
50    $self->{locator} = $locator;
51    my $differ = CGI::Wiki::Plugin::Diff->new;
52    $wiki->register_plugin( plugin => $differ );
53    $self->{differ} = $differ;
54    return $self;
55}
56
57=item B<wiki>
58
59An accessor, returns the underlying L<CGI::Wiki> object.
60
61=cut
62
63sub wiki {
64    my $self = shift;
65    return $self->{wiki};
66}
67
68=item B<config>
69
70An accessor, returns the underlying L<Config::Tiny> object.
71
72=cut
73
74sub config {
75    my $self = shift;
76    return $self->{config};
77}
78
79=item B<locator>
80
81An accessor, returns the underlying L<CGI::Wiki::Plugin::Locator::UK> object.
82
83=cut
84
85sub locator {
86    my $self = shift;
87    return $self->{locator};
88}
89
90=item B<differ>
91
92An accessor, returns the underlying L<CGI::Wiki::Plugin::Diff> object.
93
94=cut
95
96sub differ {
97    my $self = shift;
98    return $self->{differ};
99}
100
101=item B<display_node>
102
103  # Print node to STDOUT.
104  $guide->display_node(
105                        id      => "Calthorpe Arms",
106                        version => 2,
107                      );
108
109  # Or return output as a string (useful for writing tests).
110  $guide->display_node(
111                        id            => "Calthorpe Arms",
112                        return_output => 1,
113                      );
114
115  # Or return the hash of variables that will be passed to the template
116  # (not including those set additionally by OpenGuides::Template).
117  $guide->display_node(
118                        id             => "Calthorpe Arms",
119                        return_tt_vars => 1,
120                      );
121
122If C<version> is omitted then the latest version will be displayed.
123
124=cut
125
126sub display_node {
127    my ($self, %args) = @_;
128    my $return_output = $args{return_output} || 0;
129    my $version = $args{version};
130    my $id = $args{id} || $self->config->{_}->{home_name};
131    my $wiki = $self->wiki;
132    my $config = $self->config;
133
134    my %tt_vars;
135
136    if ( $id =~ /^(Category|Locale) (.*)$/ ) {
137        my $type = $1;
138        $tt_vars{is_indexable_node} = 1;
139        $tt_vars{index_type} = lc($type);
140        $tt_vars{index_value} = $2;
141        $tt_vars{"rss_".lc($type)."_url"} =
142                           $config->{_}{script_name} . "?action=rss;"
143                           . lc($type) . "=" . lc(CGI->escape($2));
144    }
145
146    my %current_data = $wiki->retrieve_node( $id );
147    my $current_version = $current_data{version};
148    undef $version if ($version && $version == $current_version);
149    my %criteria = ( name => $id );
150    $criteria{version} = $version if $version;#retrieve_node default is current
151
152    my %node_data = $wiki->retrieve_node( %criteria );
153    my $raw = $node_data{content};
154    if ( $raw =~ /^#REDIRECT\s+(.+?)\s*$/ ) {
155        my $redirect = $1;
156        # Strip off enclosing [[ ]] in case this is an extended link.
157        $redirect =~ s/^\[\[//;
158        $redirect =~ s/\]\]\s*$//;
159        # See if this is a valid node, if not then just show the page as-is.
160        if ( $wiki->node_exists($redirect) ) {
161            my $output = $self->redirect_to_node($redirect);
162            return $output if $return_output;
163            print $output;
164            exit 0;
165        }
166    }
167    my $content    = $wiki->format($raw);
168    my $modified   = $node_data{last_modified};
169    my %metadata   = %{$node_data{metadata}};
170
171    my %metadata_vars = OpenGuides::Template->extract_metadata_vars(
172                            wiki     => $wiki,
173                            config   => $config,
174                            metadata => $node_data{metadata} );
175
176    %tt_vars = (
177                 %tt_vars,
178                 %metadata_vars,
179                 content       => $content,
180                 last_modified => $modified,
181                 version       => $node_data{version},
182                 node          => $id,
183                 language      => $config->{_}->{default_language},
184               );
185
186
187    # We've undef'ed $version above if this is the current version.
188    $tt_vars{current} = 1 unless $version;
189
190    if ($id eq "RecentChanges") {
191        my $minor_edits = $self->get_cookie( "show_minor_edits_in_rc" );
192        my %recent_changes;
193        my $q = CGI->new;
194        my $since = $q->param("since");
195        if ( $since ) {
196            $tt_vars{since} = $since;
197            my $t = localtime($since); # overloaded by Time::Piece
198            $tt_vars{since_string} = $t->strftime;
199            my %criteria = ( since => $since );
200            $criteria{metadata_was} = { edit_type => "Normal edit" }
201              unless $minor_edits;
202            my @rc = $self->{wiki}->list_recent_changes( %criteria );
203
204            @rc = map {
205                {
206                  name        => CGI->escapeHTML($_->{name}),
207                  last_modified => CGI->escapeHTML($_->{last_modified}),
208                  version     => CGI->escapeHTML($_->{version}),
209                  comment     => CGI->escapeHTML($_->{metadata}{comment}[0]),
210                  username    => CGI->escapeHTML($_->{metadata}{username}[0]),
211                  host        => CGI->escapeHTML($_->{metadata}{host}[0]),
212                  username_param => CGI->escape($_->{metadata}{username}[0]),
213                  edit_type   => CGI->escapeHTML($_->{metadata}{edit_type}[0]),
214                  url         => "$config->{_}->{script_name}?"
215          . CGI->escape($wiki->formatter->node_name_to_node_param($_->{name})),
216                }
217                       } @rc;
218            if ( scalar @rc ) {
219                $recent_changes{since} = \@rc;
220            }
221        } else {
222            for my $days ( [0, 1], [1, 7], [7, 14], [14, 30] ) {
223                my %criteria = ( between_days => $days );
224                $criteria{metadata_was} = { edit_type => "Normal edit" }
225                  unless $minor_edits;
226                my @rc = $self->{wiki}->list_recent_changes( %criteria );
227
228                @rc = map {
229                {
230                  name        => CGI->escapeHTML($_->{name}),
231                  last_modified => CGI->escapeHTML($_->{last_modified}),
232                  version     => CGI->escapeHTML($_->{version}),
233                  comment     => CGI->escapeHTML($_->{metadata}{comment}[0]),
234                  username    => CGI->escapeHTML($_->{metadata}{username}[0]),
235                  host        => CGI->escapeHTML($_->{metadata}{host}[0]),
236                  username_param => CGI->escape($_->{metadata}{username}[0]),
237                  edit_type   => CGI->escapeHTML($_->{metadata}{edit_type}[0]),
238                  url         => "$config->{_}->{script_name}?"
239          . CGI->escape($wiki->formatter->node_name_to_node_param($_->{name})),
240                }
241                           } @rc;
242                if ( scalar @rc ) {
243                    $recent_changes{$days->[1]} = \@rc;
244                }
245            }
246        }
247        $tt_vars{recent_changes} = \%recent_changes;
248        my %processing_args = (
249                                id            => $id,
250                                template      => "recent_changes.tt",
251                                tt_vars       => \%tt_vars,
252                               );
253        if ( !$since && $self->get_cookie("track_recent_changes_views") ) {
254            my $cookie =
255               OpenGuides::CGI->make_recent_changes_cookie(config => $config );
256            $processing_args{cookies} = $cookie;
257            $tt_vars{last_viewed} = OpenGuides::CGI->get_last_recent_changes_visit_from_cookie( config => $config );
258        }
259        return %tt_vars if $args{return_tt_vars};
260        my $output = $self->process_template( %processing_args );
261        return $output if $return_output;
262        print $output;
263    } elsif ( $id eq $self->config->{_}->{home_name} ) {
264        my @recent = $wiki->list_recent_changes(
265            last_n_changes => 10,
266            metadata_was   => { edit_type => "Normal edit" },
267        );
268        @recent = map { {name          => CGI->escapeHTML($_->{name}),
269                         last_modified => CGI->escapeHTML($_->{last_modified}),
270                         comment       => CGI->escapeHTML($_->{metadata}{comment}[0]),
271                         username      => CGI->escapeHTML($_->{metadata}{username}[0]),
272                         url           => "$config->{_}->{script_name}?"
273          . CGI->escape($wiki->formatter->node_name_to_node_param($_->{name})) }
274                       } @recent;
275        $tt_vars{recent_changes} = \@recent;
276        return %tt_vars if $args{return_tt_vars};
277        my $output = $self->process_template(
278                                              id            => $id,
279                                              template      => "home_node.tt",
280                                              tt_vars       => \%tt_vars,
281                                            );
282        return $output if $return_output;
283        print $output;
284    } else {
285        return %tt_vars if $args{return_tt_vars};
286        my $output = $self->process_template(
287                                              id            => $id,
288                                              template      => "node.tt",
289                                              tt_vars       => \%tt_vars,
290                                            );
291        return $output if $return_output;
292        print $output;
293    }
294}
295
296=item B<display_diffs>
297
298  $guide->display_diffs(
299                         id            => "Home Page",
300                         version       => 6,
301                         other_version => 5,
302                       );
303
304  # Or return output as a string (useful for writing tests).
305  my $output = $guide->display_diffs(
306                                      id            => "Home Page",
307                                      version       => 6,
308                                      other_version => 5,
309                                      return_output => 1,
310                                    );
311
312  # Or return the hash of variables that will be passed to the template
313  # (not including those set additionally by OpenGuides::Template).
314  my %vars = $guide->display_diffs(
315                                    id             => "Home Page",
316                                    version        => 6,
317                                    other_version  => 5,
318                                    return_tt_vars => 1,
319                                  );
320
321=cut
322
323sub display_diffs {
324    my ($self, %args) = @_;
325    my %diff_vars = $self->differ->differences(
326                                        node          => $args{id},
327                                        left_version  => $args{version},
328                                        right_version => $args{other_version},
329                                              );
330    $diff_vars{not_deletable} = 1;
331    $diff_vars{not_editable} = 1;
332    $diff_vars{deter_robots} = 1;
333    return %diff_vars if $args{return_tt_vars};
334    my $output = $self->process_template(
335                                          id       => $args{id},
336                                          template => "differences.tt",
337                                          tt_vars  => \%diff_vars
338                                        );
339    return $output if $args{return_output};
340    print $output;
341}
342
343=item B<find_within_distance>
344
345  $guide->find_within_distance(
346                                id => $node,
347                                metres => $q->param("distance_in_metres")
348                              );
349
350=cut
351
352sub find_within_distance {
353    my ($self, %args) = @_;
354    my $node = $args{id};
355    my $metres = $args{metres};
356    my %data = $self->wiki->retrieve_node( $node );
357    my $lat = $data{metadata}{latitude}[0];
358    my $long = $data{metadata}{longitude}[0];
359    my $script_url = $self->config->{_}{script_url};
360    print CGI->redirect( $script_url . "supersearch.cgi?lat=$lat;long=$long;distance_in_metres=$metres" );
361}
362
363=item B<show_index>
364
365  $guide->show_index(
366                      type   => "category",
367                      value  => "pubs",
368                    );
369
370  # RDF version.
371  $guide->show_index(
372                      type   => "locale",
373                      value  => "Holborn",
374                      format => "rdf",
375                    );
376
377  # Or return output as a string (useful for writing tests).
378  $guide->show_index(
379                      type          => "category",
380                      value         => "pubs",
381                      return_output => 1,
382                    );
383
384=cut
385
386sub show_index {
387    my ($self, %args) = @_;
388    my $wiki = $self->wiki;
389    my $formatter = $wiki->formatter;
390    my %tt_vars;
391    my @selnodes;
392
393    if ( $args{type} and $args{value} ) {
394        if ( $args{type} eq "fuzzy_title_match" ) {
395            my %finds = $wiki->fuzzy_title_match( $args{value} );
396            @selnodes = sort { $finds{$a} <=> $finds{$b} } keys %finds;
397            $tt_vars{criterion} = {
398                type  => $args{type},  # for RDF version
399                value => $args{value}, # for RDF version
400                name  => CGI->escapeHTML("Fuzzy Title Match on '$args{value}'"),
401                not_editable => 1
402            };
403        } else {
404            @selnodes = $wiki->list_nodes_by_metadata(
405                metadata_type  => $args{type},
406                metadata_value => $args{value},
407                ignore_case    => 1,
408            );
409            my $name = ucfirst($args{type}) . " $args{value}" ;
410            my $url = $self->config->{_}->{script_name}
411                      . "?"
412                      . ucfirst( $args{type} )
413                      . "_"
414                      . uri_escape(
415                              $formatter->node_name_to_node_param($args{value})
416                                  );
417            $tt_vars{criterion} = {
418                type  => $args{type},
419                value => $args{value}, # for RDF version
420                name  => CGI->escapeHTML( $name ),
421                url   => $url,
422                not_editable => 1
423            };
424        }
425    } else {
426        @selnodes = $wiki->list_all_nodes();
427    }
428
429    my @nodes = map { { name      => $_,
430                        node_data => { $wiki->retrieve_node( name => $_ ) },
431                        param     => $formatter->node_name_to_node_param($_) }
432                    } sort @selnodes;
433
434    $tt_vars{nodes} = \@nodes;
435
436    my ($template, %conf);
437
438    if ( $args{format} and $args{format} eq "rdf" ) {
439        $template = "rdf_index.tt";
440        $conf{content_type} = "text/plain";
441    } else {
442        $template = "site_index.tt";
443    }
444
445    %conf = (
446              %conf,
447              node        => "$args{type} index", # KLUDGE
448              template    => $template,
449              tt_vars     => \%tt_vars,
450    );
451
452    my $output = $self->process_template( %conf );
453    return $output if $args{return_output};
454    print $output;
455}
456
457=item B<list_all_versions>
458
459  $guide->list_all_versions ( id => "Home Page" );
460
461  # Or return output as a string (useful for writing tests).
462  $guide->list_all_versions (
463                              id            => "Home Page",
464                              return_output => 1,
465                            );
466
467  # Or return the hash of variables that will be passed to the template
468  # (not including those set additionally by OpenGuides::Template).
469  $guide->list_all_versions (
470                              id             => "Home Page",
471                              return_tt_vars => 1,
472                            );
473
474=cut
475
476sub list_all_versions {
477    my ($self, %args) = @_;
478    my $return_output = $args{return_output} || 0;
479    my $node = $args{id};
480    my %curr_data = $self->wiki->retrieve_node($node);
481    my $curr_version = $curr_data{version};
482    croak "This is the first version" unless $curr_version > 1;
483    my @history;
484    for my $version ( 1 .. $curr_version ) {
485        my %node_data = $self->wiki->retrieve_node( name    => $node,
486                                                    version => $version );
487        # $node_data{version} will be zero if this version was deleted.
488        push @history, {
489            version  => CGI->escapeHTML( $version ),
490            modified => CGI->escapeHTML( $node_data{last_modified} ),
491            username => CGI->escapeHTML( $node_data{metadata}{username}[0] ),
492            comment  => CGI->escapeHTML( $node_data{metadata}{comment}[0] ),
493                       } if $node_data{version};
494    }
495    @history = reverse @history;
496    my %tt_vars = ( node          => $node,
497                    version       => $curr_version,
498                    not_deletable => 1,
499                    not_editable  => 1,
500                    deter_robots  => 1,
501                    history       => \@history );
502    return %tt_vars if $args{return_tt_vars};
503    my $output = $self->process_template(
504                                          id       => $node,
505                                          template => "node_history.tt",
506                                          tt_vars  => \%tt_vars,
507                                        );
508    return $output if $return_output;
509    print $output;
510}
511
512=item B<commit_node>
513
514  $guide->commit_node(
515                       id      => $node,
516                       cgi_obj => $q,
517                     );
518
519As with other methods, parameters C<return_tt_vars> and
520C<return_output> can be used to return these things instead of
521printing the output to STDOUT.
522
523=cut
524
525sub commit_node {
526    my ($self, %args) = @_;
527    my $node = $args{id};
528    my $q = $args{cgi_obj};
529    my $wiki = $self->wiki;
530    my $config = $self->config;
531
532    my $content  = $q->param("content");
533    $content =~ s/\r\n/\n/gs;
534    my $checksum = $q->param("checksum");
535
536    my %metadata = OpenGuides::Template->extract_metadata_vars(
537        wiki    => $wiki,
538        config  => $config,
539        cgi_obj => $q
540    );
541
542    $metadata{opening_hours_text} = $q->param("hours_text") || "";
543
544    # Check to make sure all the indexable nodes are created
545    foreach my $type (qw(Category Locale)) {
546        my $lctype = lc($type);
547        foreach my $index (@{$metadata{$lctype}}) {
548            $index =~ s/(.*)/\u$1/;
549            my $node = $type . " " . $index;
550            # Uppercase the node name before checking for existence
551            $node =~ s/ (\S+)/ \u$1/g;
552            unless ( $wiki->node_exists($node) ) {
553                my $category = $type eq "Category" ? "Category" : "Locales";
554                $wiki->write_node( $node,
555                                   "\@INDEX_LINK [[$node]]",
556                                   undef,
557                                   { username => "Auto Create",
558                                     comment  => "Auto created $lctype stub page",
559                                     category => $category
560                                   }
561                );
562            }
563        }
564    }
565       
566    foreach my $var ( qw( username comment edit_type ) ) {
567        $metadata{$var} = $q->param($var) || "";
568    }
569    $metadata{host} = $ENV{REMOTE_ADDR};
570
571    # CGI::Wiki::Plugin::RSS::ModWiki wants "major_change" to be set.
572    $metadata{major_change} = ( $metadata{edit_type} eq "Normal edit" )
573                            ? 1
574                            : 0;
575
576    my $written = $wiki->write_node($node, $content, $checksum, \%metadata );
577
578    if ($written) {
579        print $self->redirect_to_node($node);
580    } else {
581        my %node_data = $wiki->retrieve_node($node);
582        my %tt_vars = ( checksum       => $node_data{checksum},
583                        new_content    => $content,
584                        stored_content => $node_data{content} );
585        foreach my $mdvar ( keys %metadata ) {
586            if ($mdvar eq "locales") {
587                $tt_vars{"stored_$mdvar"} = $node_data{metadata}{locale};
588                $tt_vars{"new_$mdvar"}    = $metadata{locale};
589            } elsif ($mdvar eq "categories") {
590                $tt_vars{"stored_$mdvar"} = $node_data{metadata}{category};
591                $tt_vars{"new_$mdvar"}    = $metadata{category};
592            } elsif ($mdvar eq "username" or $mdvar eq "comment"
593                      or $mdvar eq "edit_type" ) {
594                $tt_vars{$mdvar} = $metadata{$mdvar};
595            } else {
596                $tt_vars{"stored_$mdvar"} = $node_data{metadata}{$mdvar}[0];
597                $tt_vars{"new_$mdvar"}    = $metadata{$mdvar};
598            }
599        }
600        return %tt_vars if $args{return_tt_vars};
601        my $output = $self->process_template(
602                                              id       => $node,
603                                              template => "edit_conflict.tt",
604                                              tt_vars  => \%tt_vars,
605                                            );
606        return $output if $args{return_output};
607        print $output;
608    }
609}
610
611
612=item B<delete_node>
613
614  $guide->delete_node(
615                       id       => "FAQ",
616                       version  => 15,
617                       password => "beer",
618                     );
619
620C<version> is optional - if it isn't supplied then all versions of the
621node will be deleted; in other words the node will be entirely
622removed.
623
624If C<password> is not supplied then a form for entering the password
625will be displayed.
626
627=cut
628
629sub delete_node {
630    my ($self, %args) = @_;
631    my $node = $args{id} or croak "No node ID supplied for deletion";
632
633    my %tt_vars = (
634                    not_editable  => 1,
635                    not_deletable => 1,
636                    deter_robots  => 1,
637                  );
638    $tt_vars{delete_version} = $args{version} || "";
639
640    my $password = $args{password};
641
642    if ($password) {
643        if ($password ne $self->config->{_}->{admin_pass}) {
644            print $self->process_template(
645                                     id       => $node,
646                                     template => "delete_password_wrong.tt",
647                                     tt_vars  => \%tt_vars,
648                                   );
649        } else {
650            $self->wiki->delete_node(
651                                      name    => $node,
652                                      version => $args{version},
653                                    );
654            # Check whether any versions of this node remain.
655            my %check = $self->wiki->retrieve_node( name => $node );
656            $tt_vars{other_versions_remain} = 1 if $check{version};
657            print $self->process_template(
658                                     id       => $node,
659                                     template => "delete_done.tt",
660                                     tt_vars  => \%tt_vars,
661                                   );
662        }
663    } else {
664        print $self->process_template(
665                                 id       => $node,
666                                 template => "delete_confirm.tt",
667                                 tt_vars  => \%tt_vars,
668                               );
669    }
670}
671
672sub process_template {
673    my ($self, %args) = @_;
674    my %output_conf = ( wiki     => $self->wiki,
675                        config   => $self->config,
676                        node     => $args{id},
677                        template => $args{template},
678                        vars     => $args{tt_vars},
679                        cookies  => $args{cookies},
680    );
681    if ( $args{content_type} ) {
682        $output_conf{content_type} = "";
683        my $output = "Content-Type: $args{content_type}\n\n"
684                     . OpenGuides::Template->output( %output_conf );
685    } else {
686        return OpenGuides::Template->output( %output_conf );
687    }
688}
689
690sub redirect_to_node {
691    my ($self, $node) = @_;
692    my $script_url = $self->config->{_}->{script_url};
693    my $script_name = $self->config->{_}->{script_name};
694    my $formatter = $self->wiki->formatter;
695    my $param = $formatter->node_name_to_node_param( $node );
696    return CGI->redirect( "$script_url$script_name?$param" );
697}
698
699sub get_cookie {
700    my $self = shift;
701    my $config = $self->config;
702    my $pref_name = shift or return "";
703    my %cookie_data = OpenGuides::CGI->get_prefs_from_cookie(config=>$config);
704    return $cookie_data{$pref_name};
705}
706
707
708=back
709
710=head1 BUGS AND CAVEATS
711
712At the moment, the location data uses a United-Kingdom-specific module,
713so the location features might not work so well outside the UK.
714
715=head1 SEE ALSO
716
717=over 4
718
719=item * L<http://london.openguides.org/|The Open Guide to London>, the first and biggest OpenGuides site.
720
721=item * L<http://openguides.org/|The OpenGuides website>, with a list of all live OpenGuides installs.
722
723=item * L<CGI::Wiki>, the Wiki toolkit which does the heavy lifting for OpenGuides
724
725=back
726
727=head1 FEEDBACK
728
729If you have a question, a bug report, or a patch, or you're interested
730in joining the development team, please contact openguides-dev@openguides.org
731(moderated mailing list, will reach all current developers but you'll have
732to wait for your post to be approved) or kake@earth.li (a real person who
733may take a little while to reply to your mail if she's busy).
734
735=head1 AUTHOR
736
737The OpenGuides Project (openguides-dev@openguides.org)
738
739=head1 COPYRIGHT
740
741     Copyright (C) 2003-4 The OpenGuides Project.  All Rights Reserved.
742
743The OpenGuides distribution is free software; you can redistribute it
744and/or modify it under the same terms as Perl itself.
745
746=head1 CREDITS
747
748Programming by Dominic Hargreaves, Earle Martin, Kake Pugh, and Ivor
749Williams.  Testing and bug reporting by Billy Abbott, Jody Belka,
750Kerry Bosworth, Simon Cozens, Cal Henderson, Steve Jolly, and Bob
751Walker (among others).  Much of the Module::Build stuff copied from
752the Siesta project L<http://siesta.unixbeard.net/>
753
754=cut
755
7561;
Note: See TracBrowser for help on using the repository browser.