File Coverage

blib/lib/MojoMojo/Controller/Page.pm
Criterion Covered Total %
statement 207 259 79.9
branch 22 56 39.2
condition 13 24 54.1
subroutine 40 41 97.5
pod 16 16 100.0
total 298 396 75.2


line stmt bran cond sub pod time code
1             package MojoMojo::Controller::Page;
2              
3 35     35   16810 use strict;
  35         100  
  35         1112  
4 35     35   186 use parent 'Catalyst::Controller';
  35         74  
  35         220  
5 35     35   17478 use IO::Scalar;
  35         105408  
  35         1393  
6 35     35   256 use URI;
  35         84  
  35         374  
7 35     35   14496 use Text::Context;
  35         30633  
  35         356  
8 35     35   15847 use HTML::Strip;
  35         32599  
  35         345  
9 35     35   14416 use Data::Page;
  35         56691  
  35         248  
10 35     35   1159 use Data::Dumper;
  35         85  
  35         9625  
11              
12             =head1 NAME
13              
14             MojoMojo::Controller::Page - Page controller
15              
16             =head1 SYNOPSIS
17              
18             =head1 DESCRIPTION
19              
20             This controller is the main juice of MojoMojo. It handles all the
21             actions related to wiki pages. Actions are redispatched to this
22             controller based on MojoMojo's custom prepare_path method.
23              
24             Every private action here expects to have a page path in args. They
25             can be called with urls like "/page1/page2.action".
26              
27             =head1 ACTIONS
28              
29             =head2 view (.view)
30              
31             This is probably the most common action in MojoMojo. A lot of the
32             other actions redispatch to this one. It will prepare the stash
33             for page view, and set the template to C<view.tt>, unless another is
34             already set.
35              
36             It also takes an optional 'rev' parameter, in which case it will
37             load the provided revision instead.
38              
39             =cut
40              
41             sub view : Global {
42 28     28   29523 my ( $self, $c, $path ) = @_;
43              
44 28         140 my $stash = $c->stash;
45 28   100     1854 $stash->{template} ||= 'page/view.tt';
46              
47 28         156 $c->forward('inline_tags');
48              
49             # FIXME NOTE: Highlight has been turned off until someone makes it work perfectly in all cases.
50             # In particular it sucks with TOC and valid HTML
51             # $c->stash->{render} = 'highlight'
52             # if $c->req->referer && $c->req->referer =~ /.edit$/;
53              
54             my ( $path_pages, $proto_pages, $id ) =
55 28         5719 @$stash{qw/ path_pages proto_pages id /};
56              
57             # we should always have at least "/" in path pages. if we don't,
58             # we must not have had these structures in the stash
59              
60 28 50 33     272 return $c->forward('suggest')
61             if $proto_pages && @$proto_pages;
62              
63 28         83 my $page = $stash->{page};
64              
65 28 50       185 return unless $c->check_view_permission;
66              
67 28         81 my $content;
68              
69 28         163 my $rev = $c->req->params->{rev};
70 28 100 66     2921 if ( $rev && defined $page->content_version ) {
71 2         57 $content = $c->model("DBIC::Content")->find(
72             {
73             page => $page->id,
74             version => $rev
75             }
76             );
77 2 100       9591 $stash->{rev} = ( defined $content ? $content->version : undef );
78 2 100       108 unless ( $stash->{rev} ) {
79 1         16 $stash->{message} = $c->loc(
80             'No revision x for x',
81             $rev,
82             '<span class="error_detail">'
83             . '<a href="'
84             . $page->path . '">'
85             . $page->name . '</a>'
86             . '</span>'
87             );
88 1         1009 $stash->{template} = 'message.tt';
89             }
90             }
91             else {
92 26         389 $content = $page->content;
93 26 50       141695 unless ($content) {
94 0         0 $c->detach('/pageadmin/edit');
95              
96             }
97 26         647 $stash->{rev} = $content->version;
98             }
99              
100             # cache a precompiled version when missing
101 28 100 100     1256 if ( $content && not defined $content->precompiled ) {
102 1         33 my $precomp_body = $content->body;
103 1         22 MojoMojo->call_plugins( 'format_content', \$precomp_body, $c, $page );
104 1         33 $content->precompiled( $precomp_body );
105 1         143 $content->update;
106             }
107              
108 28         11499 $stash->{content} = $content;
109 35     35   254 }
  35         90  
  35         270  
110              
111             =head2 search (.search)
112              
113             This action is called as C<.search> on the current page when the user
114             performs a search. The user can choose to search the entire site or a
115             subtree starting from the current page.
116              
117             =cut
118              
119             sub search : Global {
120 3     3 1 2541 my ( $self, $c ) = @_;
121              
122 3         16 my $stash = $c->stash;
123              
124             # number of search results to show per page
125 3         171 my $results_per_page = 10;
126              
127 3         12 my $page = $c->stash->{page};
128              
129             # $q represents the search query
130 3   50     162 my $q = $c->req->params->{q} || $c->stash->{query} || q();
131 3   50     403 my $search_type = $c->req->params->{search_type} || "subtree";
132 3         199 $stash->{query} = $q;
133 3         12 $stash->{search_type} = $search_type;
134              
135 3         29 my $strip = HTML::Strip->new;
136              
137 3         279 my $results = [];
138              
139             # For subtree searches, we'll use the (modified) page path to restrict the search hits.
140 3         8 my $fixed_path;
141 3 50       14 if ( $search_type eq "subtree" ) {
142 3         21 $fixed_path = $page->path;
143              
144             # Replace slashes with X so fixed path format matches hit path format.
145 3         66 $fixed_path =~ s{/}{X}g;
146             }
147              
148 3         16 my $hits = $c->model('Search')->search($q);
149 3         9 my %results_hash;
150 3         20 while ( my $hit = $hits->fetch_hit_hashref ) {
151              
152             # Filter out hits that aren't part of subtree
153 0 0       0 if ( $search_type eq 'subtree' ) {
154 0 0       0 next if $hit->{path} !~ m/$fixed_path/mx;
155             }
156 0         0 $hit->{path} =~ s{X}{/}g;
157 0         0 my ($path_pages) = $c->model('DBIC::Page')->path_pages( $hit->{path} );
158 0         0 my $page = $path_pages->[ @$path_pages - 1 ];
159              
160             # skip search result depending on permissions
161 0         0 my $user;
162 0 0       0 if ( $c->pref('check_permission_on_view') ) {
163 0 0       0 if ( $c->user_exists() ) { $user = $c->user->obj; }
  0         0  
164 0         0 my $perms = $c->check_permissions( $page->path, $user );
165 0 0       0 next unless $perms->{view};
166             }
167              
168             # add a snippet of text containing the search query
169 0   0     0 my $content = $strip->parse( $page->content->precompiled || $page->content->formatted($c) );
170 0         0 $strip->eof;
171              
172             # FIXME: Bug? Some snippet text doesn't get displayed properly by Text::Context
173 0         0 my $snippet = Text::Context->new( $content, split( / /, $q ) );
174              
175             # Store goods to be used in search results listing
176             # NOTE: $page->path is '/' for app root,
177             # but $c->request->path is empty for app root.
178 0         0 my $title_base_nodes;
179 0 0       0 if ( $page->path ne '/' ) {
180 0         0 ($title_base_nodes) = $page->path =~ m{(.*/).*$};
181 0         0 $title_base_nodes =~ s{^/}{};
182 0         0 $title_base_nodes =~ s{/}{ > }g;
183             }
184             $results_hash{ $hit->{path} } = {
185             snippet => $snippet->as_html,
186             page => $page,
187             score => $hit->{score},
188 0         0 title_base_nodes => $title_base_nodes,
189             };
190              
191             }
192              
193             # Order hits by score.
194 3         818 my @results;
195 3         15 foreach my $hit_path (
196 0         0 sort { $results_hash{$b}->{score} <=> $results_hash{$a}->{score} }
197             keys %results_hash
198             )
199             {
200 0         0 push @results, $results_hash{$hit_path};
201             }
202 3         9 $results = \@results;
203 3         9 my $result_count = scalar @$results;
204 3 50       12 if ($result_count) {
205              
206             # Paginate the results.
207             # This is done even with 1 page of results so the template doesn't need
208             # to do two separate things.
209 0         0 my $pager = Data::Page->new;
210 0         0 $pager->total_entries($result_count);
211 0         0 $pager->entries_per_page($results_per_page);
212 0   0     0 $pager->current_page( $c->req->params->{p} || 1 );
213              
214 0 0       0 if ( $result_count > $results_per_page ) {
215              
216             # trim down the results to just this page
217 0         0 @$results = $pager->splice($results);
218             }
219              
220 0         0 $c->stash->{pager} = $pager;
221 0 0       0 my $last_page = ( $pager->last_page > 10 ) ? 10 : $pager->last_page;
222 0         0 $c->stash->{pages_to_link} = [ 1 .. $last_page ];
223 0         0 $c->stash->{results} = $results;
224 0         0 $c->stash->{result_count} = $result_count;
225             }
226 3         66 $stash->{template} = 'page/search.tt';
227 35     35   448876 }
  35         87  
  35         191  
228              
229             =head2 print
230              
231             This action is the same as the L</view> action, but with a printer-friendly
232             template.
233              
234             =cut
235              
236             sub print : Global {
237 1     1 1 1021 my ( $self, $c, $page ) = @_;
238 1         5 $c->stash->{template} = 'page/print.tt';
239 1         63 $c->forward('view');
240 35     35   33423 }
  35         86  
  35         166  
241              
242             =head2 inline
243              
244             Same as L</view> action, but with a template that only outputs the barebones
245             body of the page. There are no headers, footers, or navigation bars. Useful
246             for transclusion (see L<MojoMojo::Formatter::Include>).
247              
248             =cut
249              
250             sub inline : Global {
251 1     1 1 1001 my ( $self, $c, $page ) = @_;
252 1         5 $c->stash->{template} = 'page/inline.tt';
253 1         84 $c->forward('view');
254 35     35   31587 }
  35         96  
  35         170  
255              
256             =head2 inline_tags (.inline_tags)
257              
258             Tag list for the bottom of page views.
259              
260             =cut
261              
262             sub inline_tags : Global {
263 35     35 1 17971 my ( $self, $c, $highlight ) = @_;
264 35   100     170 $c->stash->{template} ||= 'page/tags.tt';
265 35         2354 $c->stash->{highlight} = $highlight;
266 35         2160 my $page = $c->stash->{page};
267 35 100       2195 if ( $c->user_exists ) {
268 13         2152 my @tags = $page->others_tags( $c->user->obj->id );
269 13         307 $c->stash->{others_tags} = [@tags];
270 13         1106 @tags = $page->user_tags( $c->user->obj->id );
271 13         322 $c->stash->{taglist} = ' ' . join( ' ', map { $_->tag } @tags ) . ' ';
  13         436  
272 13         1224 $c->stash->{tags} = [@tags];
273             }
274             else {
275 22         20760 $c->stash->{others_tags} = [ $page->tags_with_counts ];
276             }
277 35     35   34904 }
  35         90  
  35         160  
278              
279             =head2 pages_viewable($c, $user, @pages)
280              
281             Filters an array of pages, returning only those that the given user has
282             permission to view.
283              
284             =cut
285              
286             sub pages_viewable {
287 6     6 1 25 my ( $c, $user, @pages ) = @_;
288 6         21 return grep { $c->check_permissions( $_->path, $user )->{view}; } @pages;
  20         672  
289             }
290              
291             =head2 list (.list)
292              
293             All nodes in this namespace. Computes tags, all pages, backlinks, wanted and
294             orphan pages.
295              
296             =cut
297              
298             sub list : Global {
299 1     1 1 1029 my ( $self, $c, $tag ) = @_;
300 1         6 my $page = $c->stash->{page};
301 1   50     60 my $resultset_page_number = $c->req->param('page') || 1;
302            
303 1         102 $c->stash->{tags} = $c->model("DBIC::Tag")->most_used();
304 1 50       500 $c->detach('/tag/list') if $tag;
305 1         23 $c->stash->{template} = 'page/list.tt';
306              
307 1         73 my $rs = $page->descendants($resultset_page_number);
308 1         33 $c->stash->{pager} = $rs->pager;
309 1         1366 my @all_pages_viewable = $rs->all;
310            
311            
312 1         4061 my @backlinks_viewable =
313             $c->model("DBIC::Link")->search( {to_page => $page->id} );
314 1 50       2669 if ( $c->pref('check_permission_on_view') ) {
315 1         185 my $user;
316 1 50       7 if ( $c->user_exists() ) { $user = $c->user->obj; }
  0         0  
317 1         908 @all_pages_viewable = pages_viewable( $c, $user, @all_pages_viewable );
318             @backlinks_viewable = grep {
319              
320             # does the user have permission to view the page from which ours is linked?
321 1         41 $c->check_permissions( $_->from_page->path, $user )->{view};
  0         0  
322             } @backlinks_viewable;
323             }
324 1         7 $c->stash->{pages} = \@all_pages_viewable;
325 1         90 $c->stash->{backlinks} = \@backlinks_viewable;
326              
327 1         56 $c->stash->{orphans} = []; # FIXME - real data here please
328              
329             # no need to check any permissions here because the user already
330             # views this page, and wanted pages are redlinks in it
331             $c->stash->{wanted} = [
332             $c->model("DBIC::WantedPage")->search(
333 1         54 { from_page => [ $page->id, map { $_->id } @all_pages_viewable ] }
  3         613  
334             )
335             ];
336 35     35   40565 }
  35         98  
  35         180  
337              
338             =head2 subtree (.subtree)
339              
340             Display all pages that are part of the subtree for the current node.
341              
342             =cut
343              
344             sub subtree : Global {
345 0     0 1 0 my ( $self, $c ) = @_;
346            
347 0         0 my $page = $c->stash->{page};
348 0         0 my @all_pages_viewable = sort { $a->{path} cmp $b->{path} } $page->descendants;
  0         0  
349 0 0       0 if ( $c->pref('check_permission_on_view') ) {
350 0         0 my $user;
351 0 0       0 if ( $c->user_exists() ) {
352 0         0 $user = $c->user->obj;
353             } else {
354             # if anonymous user is allowed
355 0         0 my $anonymous = $c->pref('anonymous_user');
356 0 0       0 if ($anonymous) {
357             # get anonymous user for no logged-in users
358 0         0 $user = $c->model('DBIC::Person') ->search( {login => $anonymous} )->first;
359             }
360             }
361 0         0 @all_pages_viewable = pages_viewable( $c, $user, @all_pages_viewable );
362             }
363 0         0 $c->stash->{pages} = \@all_pages_viewable;
364 0         0 $c->stash->{template} = 'page/subtree.tt';
365 35     35   34897 }
  35         88  
  35         166  
366              
367              
368             =head2 recent (.recent)
369              
370             Recently changed pages in this namespace. Also computes the most used
371             tags.
372              
373             =cut
374              
375             sub recent : Global {
376 5     5 1 3359 my ( $self, $c, $tag ) = @_;
377 5 50       24 $c->detach( '/tag/recent', [$tag] ) if $tag;
378 5         22 $c->stash->{tags} = $c->model("DBIC::Tag")->most_used;
379 5         2489 my $page = $c->stash->{page};
380 5         371 $c->stash->{template} = 'page/recent.tt';
381              
382 5         304 my @pages_viewable = $page->descendants_by_date;
383 5 50       58 if ( $c->pref('check_permission_on_view') ) {
384 5         992 my $user;
385 5 50       28 if ( $c->user_exists() ) { $user = $c->user->obj; }
  0         0  
386 5         4142 @pages_viewable = pages_viewable( $c, $user, @pages_viewable );
387             }
388 5         234 $c->stash->{pages} = \@pages_viewable;
389 35     35   34623 }
  35         113  
  35         173  
390              
391             =head2 feeds (.feeds)
392              
393             Overview of available feeds for this node.
394              
395             =cut
396              
397             sub feeds : Global {
398 2     2 1 2033 my ( $self, $c ) = @_;
399 2         10 $c->stash->{template} = 'feeds.tt';
400 35     35   31891 }
  35         86  
  35         172  
401              
402             =head2 rss (.rss)
403              
404             RSS feed with headlines of recent nodes in this namespace.
405              
406             =cut
407              
408             sub rss : Global {
409 1     1 1 1006 my ( $self, $c ) = @_;
410 1         6 $c->forward('recent');
411 1         214 $c->stash->{template} = 'page/rss.tt';
412 1         60 $c->res->content_type('application/rss+xml');
413 35     35   31858 }
  35         88  
  35         171  
414              
415             =head2 atom (.atom)
416              
417             Full content ATOM feed of recent nodes in this namespace.
418              
419             =cut
420              
421             sub atom : Global {
422 1     1 1 987 my ( $self, $c ) = @_;
423 1         22 $c->forward('recent');
424 1         208 $c->res->content_type('application/atom+xml');
425 1         280 $c->stash->{template} = 'page/atom.tt';
426 35     35   31623 }
  35         92  
  35         158  
427              
428             =head2 rss_full (.rss_full)
429              
430             Full content RSS feed of recent nodes in this namespace.
431              
432             =cut
433              
434             sub rss_full : Global {
435 1     1 1 1016 my ( $self, $c ) = @_;
436 1         6 $c->forward('recent');
437 1         185 $c->res->content_type('application/rss+xml');
438 1         261 $c->stash->{template} = 'page/rss_full.tt';
439 35     35   31541 }
  35         87  
  35         235  
440              
441             =head2 export (.export)
442              
443             Page showing available export options.
444              
445             =cut
446              
447             sub export : Global {
448 2     2 1 2104 my ( $self, $c ) = @_;
449 2 50       10 if ( !$c->user_exists() ) {
450 2         1645 $c->stash->{message} = $c->loc('To export, you must be logged in.');
451 2         963 $c->detach('MojoMojo::Controller::PageAdmin', 'unauthorized');
452             }
453            
454 0         0 $c->stash->{template} = 'export.tt';
455 35     35   31723 }
  35         85  
  35         174  
456              
457             =head2 suggest (.suggest)
458              
459             "Page not found" page, suggesting alternatives, and allowing creation of the page.
460             Root::auto detaches here for actions on nonexistent pages (e.g. c<bogus.export>).
461              
462             =cut
463              
464             sub suggest : Global {
465 2     2 1 1817 my ( $self, $c ) = @_;
466 2         11 $c->stash->{template} = 'page/suggest.tt';
467 2         121 $c->res->status(404);
468             # force the Catalyst flow to jump straight to the most specific 'end' action, which is Root::end
469 2         259 return 0; # otherwise, when Root::auto detaches here, we'd call the original action (e.g. 'export') too
470 35     35   32235 }
  35         89  
  35         203  
471              
472             =head2 search_inline (.search/inline)
473              
474             Search results embeddable in another page (for use with L</suggest>).
475              
476             =cut
477              
478             sub search_inline : Path('/search/inline') {
479 1     1 1 987 my ( $self, $c ) = @_;
480 1         6 $c->forward('search');
481 1         177 $c->stash->{template} = 'page/search_inline.tt';
482 35     35   32078 }
  35         92  
  35         196  
483              
484             =head2 info (.info)
485              
486             Meta information about the current page: revision list, content size, number of
487             children and descendants, links to/from, attachments.
488              
489             =cut
490              
491             sub info : Global {
492 1     1 1 1043 my ( $self, $c ) = @_;
493 1         3 my $attachments_size = 0;
494 1         2 my $attachments_count = 0;
495 1         4 foreach my $attachment ( $c->stash->{page}->attachments ) {
496 1         4985 $attachments_size+=$attachment->size;
497 1         44 $attachments_count++;
498             }
499 1         10 $c->stash->{attachments} = $attachments_count;
500 1         86 $c->stash->{attachments_size} = $attachments_size;
501 1         55 $c->stash->{body_length} = length( $c->stash->{page}->content->body );
502 1         5454 $c->stash->{template} = 'page/info.tt';
503 35     35   33483 }
  35         114  
  35         187  
504              
505             =head1 AUTHOR
506              
507             Marcus Ramberg <mramberg@cpan.org>
508              
509             =head1 LICENSE
510              
511             This library is free software. You can redistribute it and/or modify
512             it under the same terms as Perl itself.
513              
514             =cut
515              
516             1;