The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package MojoMojo::Controller::Page;

use strict;
use parent 'Catalyst::Controller';
use IO::Scalar;
use URI;
use Text::Context;
use HTML::Strip;
use Data::Page;
use Data::Dumper;

=head1 NAME

MojoMojo::Controller::Page - Page controller

=head1 SYNOPSIS

=head1 DESCRIPTION

This controller is the main juice of MojoMojo. It handles all the
actions related to wiki pages. Actions are redispatched to this
controller based on MojoMojo's custom prepare_path method.

Every private action here expects to have a page path in args. They
can be called with urls like "/page1/page2.action".

=head1 ACTIONS

=head2 view (.view)

This is probably the most common action in MojoMojo. A lot of the
other actions redispatch to this one. It will prepare the stash
for page view, and set the template to C<view.tt>, unless another is
already set.

It also takes an optional 'rev' parameter, in which case it will
load the provided revision instead.

=cut

sub view : Global {
    my ( $self, $c, $path ) = @_;

    my $stash = $c->stash;
    $stash->{template} ||= 'page/view.tt';

    $c->forward('inline_tags');

# FIXME NOTE: Highlight has been turned off until someone makes it work perfectly in all cases.
# In particular it sucks with TOC and valid HTML
# $c->stash->{render} = 'highlight'
#  if $c->req->referer && $c->req->referer =~ /.edit$/;

    my ( $path_pages, $proto_pages, $id ) =
      @$stash{qw/ path_pages proto_pages id /};

    # we should always have at least "/" in path pages. if we don't,
    # we must not have had these structures in the stash

    return $c->forward('suggest')
      if $proto_pages && @$proto_pages;

    my $page = $stash->{page};

    return unless $c->check_view_permission;

    my $content;

    my $rev = $c->req->params->{rev};
    if ( $rev && defined $page->content_version ) {
        $content = $c->model("DBIC::Content")->find(
            {
                page    => $page->id,
                version => $rev
            }
        );
        $stash->{rev} = ( defined $content ? $content->version : undef );
        unless ( $stash->{rev} ) {
            $stash->{message} = $c->loc(
                'No revision x for x',
                $rev,
                '<span class="error_detail">'
                  . '<a href="'
                  . $page->path . '">'
                  . $page->name . '</a>'
                  . '</span>'
            );
            $stash->{template} = 'message.tt';
        }
    }
    else {
        $content = $page->content;
        unless ($content) {
            $c->detach('/pageadmin/edit');

        }
        $stash->{rev} = $content->version;
    }

    # cache a precompiled version when missing 
    if ( $content && not defined $content->precompiled ) {
        my $precomp_body = $content->body;
        MojoMojo->call_plugins( 'format_content', \$precomp_body, $c, $page );
        $content->precompiled( $precomp_body );
        $content->update;
    }

    $stash->{content} = $content;
}

=head2 search (.search)

This action is called as C<.search> on the current page when the user
performs a search.  The user can choose to search the entire site or a
subtree starting from the current page.

=cut

sub search : Global {
    my ( $self, $c ) = @_;

    my $stash = $c->stash;

    # number of search results to show per page
    my $results_per_page = 10;

    my $page = $c->stash->{page};

    # $q represents the search query
    my $q = $c->req->params->{q} || $c->stash->{query} || q();
    my $search_type = $c->req->params->{search_type} || "subtree";
    $stash->{query}       = $q;
    $stash->{search_type} = $search_type;

    my $strip = HTML::Strip->new;

    my $results = [];

# For subtree searches, we'll use the (modified) page path to restrict the search hits.
    my $fixed_path;
    if ( $search_type eq "subtree" ) {
        $fixed_path = $page->path;

# Replace slashes with X so fixed path format matches hit path format.
        $fixed_path =~ s{/}{X}g;
    }

    my $hits = $c->model('Search')->search($q);
    my %results_hash;
    while ( my $hit = $hits->fetch_hit_hashref ) {

        # Filter out hits that aren't part of subtree
        if ( $search_type eq 'subtree' ) {
            next if $hit->{path} !~ m/$fixed_path/mx;
        }
        $hit->{path} =~ s{X}{/}g;
        my ($path_pages) = $c->model('DBIC::Page')->path_pages( $hit->{path} );
        my $page = $path_pages->[ @$path_pages - 1 ];

        # skip search result depending on permissions
        my $user;
        if ( $c->pref('check_permission_on_view') ) {
            if ( $c->user_exists() ) { $user = $c->user->obj; }
            my $perms = $c->check_permissions( $page->path, $user );
            next unless $perms->{view};
        }

        # add a snippet of text containing the search query
        my $content = $strip->parse( $page->content->precompiled || $page->content->formatted($c) );
        $strip->eof;

 # FIXME: Bug? Some snippet text doesn't get displayed properly by Text::Context
        my $snippet = Text::Context->new( $content, split( / /, $q ) );

        # Store goods to be used in search results listing
        # NOTE: $page->path is '/' for app root,
        # but $c->request->path is empty for app root.
        my $title_base_nodes;
        if ( $page->path ne '/' ) {
            ($title_base_nodes) = $page->path =~ m{(.*/).*$};
            $title_base_nodes =~ s{^/}{};
            $title_base_nodes =~ s{/}{ > }g;
        }
        $results_hash{ $hit->{path} } = {
            snippet          => $snippet->as_html,
            page             => $page,
            score            => $hit->{score},
            title_base_nodes => $title_base_nodes,
        };

    }

    # Order hits by score.
    my @results;
    foreach my $hit_path (
        sort { $results_hash{$b}->{score} <=> $results_hash{$a}->{score} }
        keys %results_hash
      )
    {
        push @results, $results_hash{$hit_path};
    }
    $results = \@results;
    my $result_count = scalar @$results;
    if ($result_count) {

        # Paginate the results.
        # This is done even with 1 page of results so the template doesn't need
        # to do two separate things.
        my $pager = Data::Page->new;
        $pager->total_entries($result_count);
        $pager->entries_per_page($results_per_page);
        $pager->current_page( $c->req->params->{p} || 1 );

        if ( $result_count > $results_per_page ) {

            # trim down the results to just this page
            @$results = $pager->splice($results);
        }

        $c->stash->{pager} = $pager;
        my $last_page = ( $pager->last_page > 10 ) ? 10 : $pager->last_page;
        $c->stash->{pages_to_link} = [ 1 .. $last_page ];
        $c->stash->{results}       = $results;
        $c->stash->{result_count}  = $result_count;
    }
    $stash->{template} = 'page/search.tt';
}

=head2 print

This action is the same as the L</view> action, but with a printer-friendly
template.

=cut

sub print : Global {
    my ( $self, $c, $page ) = @_;
    $c->stash->{template} = 'page/print.tt';
    $c->forward('view');
}

=head2 inline

Same as L</view> action, but with a template that only outputs the barebones
body of the page. There are no headers, footers, or navigation bars. Useful
for transclusion (see L<MojoMojo::Formatter::Include>).

=cut

sub inline : Global {
    my ( $self, $c, $page ) = @_;
    $c->stash->{template} = 'page/inline.tt';
    $c->forward('view');
}

=head2 inline_tags (.inline_tags)

Tag list for the bottom of page views.

=cut

sub inline_tags : Global {
    my ( $self, $c, $highlight ) = @_;
    $c->stash->{template} ||= 'page/tags.tt';
    $c->stash->{highlight} = $highlight;
    my $page = $c->stash->{page};
    if ( $c->user_exists ) {
        my @tags = $page->others_tags( $c->user->obj->id );
        $c->stash->{others_tags} = [@tags];
        @tags = $page->user_tags( $c->user->obj->id );
        $c->stash->{taglist} = ' ' . join( ' ', map { $_->tag } @tags ) . ' ';
        $c->stash->{tags}    = [@tags];
    }
    else {
        $c->stash->{others_tags} = [ $page->tags_with_counts ];
    }
}

=head2 pages_viewable($c, $user, @pages)

Filters an array of pages, returning only those that the given user has
permission to view.

=cut

sub pages_viewable {
    my ( $c, $user, @pages ) = @_;
    return grep { $c->check_permissions( $_->path, $user )->{view}; } @pages;
}

=head2 list (.list)

All nodes in this namespace. Computes tags, all pages, backlinks, wanted and
orphan pages.

=cut

sub list : Global {
    my ( $self, $c, $tag ) = @_;
    my $page = $c->stash->{page};
    my $resultset_page_number = $c->req->param('page') || 1;
    
    $c->stash->{tags} = $c->model("DBIC::Tag")->most_used();
    $c->detach('/tag/list') if $tag;
    $c->stash->{template} = 'page/list.tt';

    my $rs = $page->descendants($resultset_page_number);
    $c->stash->{pager} = $rs->pager;
    my @all_pages_viewable = $rs->all;
    
    
    my @backlinks_viewable =
      $c->model("DBIC::Link")->search( {to_page => $page->id} );
    if ( $c->pref('check_permission_on_view') ) {
        my $user;
        if ( $c->user_exists() ) { $user = $c->user->obj; }
        @all_pages_viewable = pages_viewable( $c, $user, @all_pages_viewable );
        @backlinks_viewable = grep {

     # does the user have permission to view the page from which ours is linked?
            $c->check_permissions( $_->from_page->path, $user )->{view};
        } @backlinks_viewable;
    }
    $c->stash->{pages}     = \@all_pages_viewable;
    $c->stash->{backlinks} = \@backlinks_viewable;

    $c->stash->{orphans} = [];    # FIXME - real data here please

    # no need to check any permissions here because the user already
    # views this page, and wanted pages are redlinks in it
    $c->stash->{wanted} = [
        $c->model("DBIC::WantedPage")->search(
            { from_page => [ $page->id, map { $_->id } @all_pages_viewable ] }
        )
    ];
}

=head2 subtree (.subtree)

Display all pages that are part of the subtree for the current node.

=cut

sub subtree : Global {
    my ( $self, $c ) = @_;
    
    my $page = $c->stash->{page};
    my @all_pages_viewable = sort { $a->{path} cmp $b->{path} } $page->descendants;
    if ( $c->pref('check_permission_on_view') ) {
        my $user;
        if ( $c->user_exists() ) {
            $user = $c->user->obj;
        } else {
            # if anonymous user is allowed
            my $anonymous = $c->pref('anonymous_user');
            if ($anonymous) {
                # get anonymous user for no logged-in users
                $user = $c->model('DBIC::Person') ->search( {login => $anonymous} )->first;
            }
        }
        @all_pages_viewable = pages_viewable( $c, $user, @all_pages_viewable );
    }
    $c->stash->{pages}     = \@all_pages_viewable;
    $c->stash->{template} = 'page/subtree.tt';
}


=head2 recent (.recent)

Recently changed pages in this namespace. Also computes the most used
tags.

=cut

sub recent : Global {
    my ( $self, $c, $tag ) = @_;
    $c->detach( '/tag/recent', [$tag] ) if $tag;
    $c->stash->{tags} = $c->model("DBIC::Tag")->most_used;
    my $page = $c->stash->{page};
    $c->stash->{template} = 'page/recent.tt';

    my @pages_viewable = $page->descendants_by_date;
    if ( $c->pref('check_permission_on_view') ) {
        my $user;
        if ( $c->user_exists() ) { $user = $c->user->obj; }
        @pages_viewable = pages_viewable( $c, $user, @pages_viewable );
    }
    $c->stash->{pages} = \@pages_viewable;
}

=head2 feeds (.feeds)

Overview of available feeds for this node.

=cut

sub feeds : Global {
    my ( $self, $c ) = @_;
    $c->stash->{template} = 'feeds.tt';
}

=head2 rss (.rss)

RSS feed with headlines of recent nodes in this namespace.

=cut

sub rss : Global {
    my ( $self, $c ) = @_;
    $c->forward('recent');
    $c->stash->{template} = 'page/rss.tt';
    $c->res->content_type('application/rss+xml');
}

=head2 atom (.atom)

Full content ATOM feed of recent nodes in this namespace.

=cut

sub atom : Global {
    my ( $self, $c ) = @_;
    $c->forward('recent');
    $c->res->content_type('application/atom+xml');
    $c->stash->{template} = 'page/atom.tt';
}

=head2 rss_full (.rss_full)

Full content RSS feed of recent nodes in this namespace.

=cut

sub rss_full : Global {
    my ( $self, $c ) = @_;
    $c->forward('recent');
    $c->res->content_type('application/rss+xml');
    $c->stash->{template} = 'page/rss_full.tt';
}

=head2 export (.export)

Page showing available export options.

=cut

sub export : Global {
    my ( $self, $c ) = @_;
    if ( !$c->user_exists() ) {
        $c->stash->{message} = $c->loc('To export, you must be logged in.');
        $c->detach('MojoMojo::Controller::PageAdmin', 'unauthorized');
    }
    
    $c->stash->{template} = 'export.tt';
}

=head2 suggest (.suggest)

"Page not found" page, suggesting alternatives, and allowing creation of the page.
Root::auto detaches here for actions on nonexistent pages (e.g. c<bogus.export>).

=cut

sub suggest : Global {
    my ( $self, $c ) = @_;
    $c->stash->{template} = 'page/suggest.tt';
    $c->res->status(404);
    # force the Catalyst flow to jump straight to the most specific 'end' action, which is Root::end
    return 0;  # otherwise, when Root::auto detaches here, we'd call the original action (e.g. 'export') too
}

=head2 search_inline (.search/inline)

Search results embeddable in another page (for use with L</suggest>).

=cut

sub search_inline : Path('/search/inline') {
    my ( $self, $c ) = @_;
    $c->forward('search');
    $c->stash->{template} = 'page/search_inline.tt';
}

=head2 info (.info)

Meta information about the current page: revision list, content size, number of
children and descendants, links to/from, attachments.

=cut

sub info : Global {
    my ( $self, $c ) = @_;
    my $attachments_size = 0;
    my $attachments_count = 0;
    foreach my $attachment ( $c->stash->{page}->attachments ) {
        $attachments_size+=$attachment->size;
        $attachments_count++;
    }
    $c->stash->{attachments} = $attachments_count;
    $c->stash->{attachments_size} = $attachments_size;
    $c->stash->{body_length} = length( $c->stash->{page}->content->body );
    $c->stash->{template}    = 'page/info.tt';
}

=head1 AUTHOR

Marcus Ramberg <mramberg@cpan.org>

=head1 LICENSE

This library is free software. You can redistribute it and/or modify
it under the same terms as Perl itself.

=cut

1;