File Coverage: lib/Yukki/Web/Controller/Page.pm

# File Coverage

File: | lib/Yukki/Web/Controller/Page.pm
Coverage: | 32.0%

line | stmt | bran | cond | sub | pod | time | code
--- | --- | --- | --- | --- | --- | --- | ---
1 |  |  |  |  |  |  | package Yukki::Web::Controller::Page;
2 |
3 | 1 1 |  |  | 1 |  | 1095 5 | use v5.24;
4 | 1 1 1 |  |  | 1 |  | 6 2 7 | use utf8;
5 | 1 1 1 |  |  | 1 |  | 21 3 6 | use Moo;
6 |
7 |  |  |  |  |  |  | with 'Yukki::Web::Controller';
8 |
9 | 1 1 1 |  |  | 1 |  | 409 3 62 | use Try::Tiny;
10 | 1 1 1 |  |  | 1 |  | 6 3 7 | use Yukki::Error qw( http_throw );
11 |
12 | 1 1 1 |  |  | 1 |  | 355 3 6 | use namespace::clean;
13 |
14 |  |  |  |  |  |  | # ABSTRACT: controller for viewing and editing pages
15 |
16 - 26 |  |
    =head1 DESCRIPTION

    Controller for viewing and editing pages

    =head1 METHODS

    =head2 fire

    On a view request routes to L</view_page>, edit request to L</edit_page>, preview request to L</preview_page>, and attach request to L</upload_attachment>.

    =cut
27 |
28 |  |  |  |  |  |  | sub fire {
29 | 1 |  |  | 1 | 1 | 3 | my ($self, $ctx) = @_;
30 |
31 | 1 |  |  |  |  | 13 | my $action = $ctx->request->path_parameters->{action};
32 | 1 1 | 50 0 0 0 0 0 0 0 |  |  |  | 21 4 | if ($action eq 'view') { $self->view_page($ctx) }
33 | 0 |  |  |  |  | 0 | elsif ($action eq 'edit') { $self->edit_page($ctx) }
34 | 0 |  |  |  |  | 0 | elsif ($action eq 'history') { $self->view_history($ctx) }
35 | 0 |  |  |  |  | 0 | elsif ($action eq 'diff') { $self->view_diff($ctx) }
36 | 0 |  |  |  |  | 0 | elsif ($action eq 'preview') { $self->preview_page($ctx) }
37 | 0 |  |  |  |  | 0 | elsif ($action eq 'attach') { $self->upload_attachment($ctx) }
38 | 0 |  |  |  |  | 0 | elsif ($action eq 'rename') { $self->rename_page($ctx) }
39 | 0 |  |  |  |  | 0 | elsif ($action eq 'remove') { $self->remove_page($ctx) }
40 |  |  |  |  |  |  | else {
41 | 0 |  |  |  |  | 0 | http_throw('That page action does not exist.', {
42 |  |  |  |  |  |  | status => 'NotFound',
43 |  |  |  |  |  |  | });
44 |  |  |  |  |  |  | }
45 |  |  |  |  |  |  | }
46 |
47 - 51 |  |
    =head2 repo_name_and_path

    This is a helper for looking up the repository name and path for the request.

    =cut
52 |
53 |  |  |  |  |  |  | sub repo_name_and_path {
54 | 1 |  |  | 1 | 1 | 2 | my ($self, $ctx) = @_;
55 |
56 | 1 |  |  |  |  | 13 | my $repo_name = $ctx->request->path_parameters->{repository};
57 | 1 |  |  |  |  | 28 | my $path = $ctx->request->path_parameters->{page};
58 |
59 | 1 | 50 |  |  |  | 18 | if (not defined $path) {
60 |  |  |  |  |  |  | my $repo_config
61 | 1 |  |  |  |  | 14 | = $self->app->settings->repositories->{$repo_name};
62 |
63 | 1 |  |  |  |  | 6 | my $path_str = $repo_config->default_page;
64 |
65 | 1 |  |  |  |  | 5 | $path = [ split m{/}, $path_str ];
66 |  |  |  |  |  |  | }
67 |
68 | 1 |  |  |  |  | 7 | return ($repo_name, $path);
69 |  |  |  |  |  |  | }
70 |
71 - 75 |  |
    =head2 lookup_page

    Given a repository name and page, returns a L<Yukki::Model::File> for it.

    =cut
76 |
77 |  |  |  |  |  |  | sub lookup_page {
78 | 1 |  |  | 1 | 1 | 2 | my ($self, $repo_name, $page) = @_;
79 |
80 | 1 |  |  |  |  | 15 | my $repository = $self->model('Repository', { name => $repo_name });
81 |
82 | 1 |  |  |  |  | 1877 | my $final_part = pop @$page;
83 | 1 |  |  |  |  | 1 | my $filetype;
84 | 1 | 50 |  |  |  | 9 | if ($final_part =~ s/\.(?<filetype>[a-z0-9]+)$//) {
85 | 1 |  |  |  |  | 15 | $filetype = $+{filetype};
86 |  |  |  |  |  |  | }
87 |
88 | 1 |  |  |  |  | 3 | my $path = join '/', @$page, $final_part;
89 | 1 |  |  |  |  | 4 | return $repository->file({ path => $path, filetype => $filetype });
90 |  |  |  |  |  |  | }
91 |
92 - 97 |  |
    =head2 view_page

    Tells either L<Yukki::Web::View::Page/blank> or L<Yukki::Web::View::Page/view>
    to show the page.

    =cut
98 |
99 |  |  |  |  |  |  | sub view_page {
100 | 1 |  |  | 1 | 1 | 2 | my ($self, $ctx) = @_;
101 |
102 | 1 |  |  |  |  | 3 | my ($repo_name, $path) = $self->repo_name_and_path($ctx);
103 |
104 | 1 |  |  |  |  | 3 | my $page = $self->lookup_page($repo_name, $path);
105 |
106 | 1 |  |  |  |  | 99 | my $breadcrumb = $self->breadcrumb($page->repository, $path);
107 |
108 | 1 |  |  |  |  | 1 | my $body;
109 | 1 | 50 |  |  |  | 4 | if (not $page->exists) {
110 | 0 |  |  |  |  | 0 | my @files = $page->list_files;
111 |
112 | 0 |  |  |  |  | 0 | $body = $self->view('Page')->blank($ctx, {
113 |  |  |  |  |  |  | title => $page->file_name,
114 |  |  |  |  |  |  | breadcrumb => $breadcrumb,
115 |  |  |  |  |  |  | repository => $repo_name,
116 |  |  |  |  |  |  | page => $page->full_path,
117 |  |  |  |  |  |  | files => \@files,
118 |  |  |  |  |  |  | });
119 |  |  |  |  |  |  | }
120 |
121 |  |  |  |  |  |  | else {
122 | 1 |  |  |  |  | 119 | $body = $self->view('Page')->view($ctx, {
123 |  |  |  |  |  |  | title => $page->title,
124 |  |  |  |  |  |  | breadcrumb => $breadcrumb,
125 |  |  |  |  |  |  | repository => $repo_name,
126 |  |  |  |  |  |  | page => $page->full_path,
127 |  |  |  |  |  |  | file => $page,
128 |  |  |  |  |  |  | });
129 |  |  |  |  |  |  | }
130 |
131 | 1 |  |  |  |  | 37584 | $ctx->response->body($body);
132 |  |  |  |  |  |  | }
133 |
134 - 138 |  |
    =head2 edit_page

    Displays or processes the edit form for a page using.

    =cut
139 |
140 |  |  |  |  |  |  | sub edit_page {
141 | 0 |  |  | 0 | 1 | 0 | my ($self, $ctx) = @_;
142 |
143 | 0 |  |  |  |  | 0 | my ($repo_name, $path) = $self->repo_name_and_path($ctx);
144 |
145 | 0 |  |  |  |  | 0 | my $page = $self->lookup_page($repo_name, $path);
146 |
147 | 0 |  |  |  |  | 0 | my $breadcrumb = $self->breadcrumb($page->repository, $path);
148 |
149 | 0 | 0 |  |  |  | 0 | if ($ctx->request->method eq 'POST') {
150 | 0 |  |  |  |  | 0 | my $new_content = $ctx->request->parameters->{yukkitext};
151 | 0 |  |  |  |  | 0 | my $position = $ctx->request->parameters->{yukkitext_position};
152 | 0 |  |  |  |  | 0 | my $comment = $ctx->request->parameters->{comment};
153 |
154 | 0 | 0 |  |  |  | 0 | if (my $user = $ctx->session->{user}) {
155 | 0 |  |  |  |  | 0 | $page->author_name($user->{name});
156 | 0 |  |  |  |  | 0 | $page->author_email($user->{email});
157 |  |  |  |  |  |  | }
158 |
159 |  |  |  |  |  |  | $page->store({
160 | 0 |  |  |  |  | 0 | content => $new_content,
161 |  |  |  |  |  |  | comment => $comment,
162 |  |  |  |  |  |  | });
163 |
164 | 0 |  |  |  |  | 0 | $ctx->response->redirect(join '/', '/page/edit', $repo_name, $page->full_path, '?yukkitext_position='.$position);
165 | 0 |  |  |  |  | 0 | return;
166 |  |  |  |  |  |  | }
167 |
168 | 0 0 |  |  |  |  | 0 0 | my @attachments = grep { $_->filetype ne 'yukki' } $page->list_files;
169 | 0 |  | 0 |  |  | 0 | my $position = $ctx->request->parameters->{yukkitext_position} // -1;
170 |
171 | 0 |  |  |  |  | 0 | $ctx->response->body(
172 |  |  |  |  |  |  | $self->view('Page')->edit($ctx, {
173 |  |  |  |  |  |  | title => $page->title,
174 |  |  |  |  |  |  | breadcrumb => $breadcrumb,
175 |  |  |  |  |  |  | repository => $repo_name,
176 |  |  |  |  |  |  | page => $page->full_path,
177 |  |  |  |  |  |  | position => $position,
178 |  |  |  |  |  |  | file => $page,
179 |  |  |  |  |  |  | attachments => \@attachments,
180 |  |  |  |  |  |  | })
181 |  |  |  |  |  |  | );
182 |  |  |  |  |  |  | }
183 |
184 - 188 |  |
    =head2 rename_page

    Displays the rename page form.

    =cut
189 |
190 |  |  |  |  |  |  | sub rename_page {
191 | 0 |  |  | 0 | 1 | 0 | my ($self, $ctx) = @_;
192 |
193 | 0 |  |  |  |  | 0 | my ($repo_name, $path) = $self->repo_name_and_path($ctx);
194 |
195 | 0 |  |  |  |  | 0 | my $page = $self->lookup_page($repo_name, $path);
196 |
197 | 0 |  |  |  |  | 0 | my $breadcrumb = $self->breadcrumb($page->repository, $path);
198 |
199 | 0 | 0 |  |  |  | 0 | if ($ctx->request->method eq 'POST') {
200 | 0 |  |  |  |  | 0 | my $new_name = $ctx->request->parameters->{yukkiname_new};
201 |
202 | 0 |  |  |  |  | 0 | my $part = qr{[_a-z0-9-.]+(?:\.[_a-z0-9-]+)*}i;
203 | 0 | 0 |  |  |  | 0 | if ($new_name =~ m{^$part(?:/$part)*$}) {
204 |
205 | 0 | 0 |  |  |  | 0 | if (my $user = $ctx->session->{user}) {
206 | 0 |  |  |  |  | 0 | $page->author_name($user->{name});
207 | 0 |  |  |  |  | 0 | $page->author_email($user->{email});
208 |  |  |  |  |  |  | }
209 |
210 |  |  |  |  |  |  | $page->rename({
211 | 0 |  |  |  |  | 0 | full_path => $new_name,
212 |  |  |  |  |  |  | comment => 'Renamed ' . $page->full_path . ' to ' . $new_name,
213 |  |  |  |  |  |  | });
214 |
215 | 0 |  |  |  |  | 0 | $ctx->response->redirect(join '/', '/page/edit', $repo_name, $new_name);
216 | 0 |  |  |  |  | 0 | return;
217 |
218 |  |  |  |  |  |  | }
219 |  |  |  |  |  |  | else {
220 | 0 |  |  |  |  | 0 | $ctx->add_errors('the new name must contain only letters, numbers, underscores, dashes, periods, and slashes');
221 |  |  |  |  |  |  | }
222 |  |  |  |  |  |  | }
223 |
224 |  |  |  |  |  |  | $ctx->response->body(
225 | 0 |  |  |  |  | 0 | $self->view('Page')->rename($ctx, {
226 |  |  |  |  |  |  | title => $page->title,
227 |  |  |  |  |  |  | breadcrumb => $breadcrumb,
228 |  |  |  |  |  |  | repository => $repo_name,
229 |  |  |  |  |  |  | page => $page->full_path,
230 |  |  |  |  |  |  | file => $page,
231 |  |  |  |  |  |  | })
232 |  |  |  |  |  |  | );
233 |  |  |  |  |  |  | }
234 |
235 - 239 |  |
    =head2 remove_page

    Displays the remove confirmation.

    =cut
240 |
241 |  |  |  |  |  |  | sub remove_page {
242 | 0 |  |  | 0 | 1 | 0 | my ($self, $ctx) = @_;
243 |
244 | 0 |  |  |  |  | 0 | my ($repo_name, $path) = $self->repo_name_and_path($ctx);
245 |
246 | 0 |  |  |  |  | 0 | my $page = $self->lookup_page($repo_name, $path);
247 |
248 | 0 |  |  |  |  | 0 | my $breadcrumb = $self->breadcrumb($page->repository, $path);
249 |
250 | 0 |  |  |  |  | 0 | my $confirmed = $ctx->request->body_parameters->{confirmed};
251 | 0 | 0 | 0 |  |  | 0 | if ($ctx->request->method eq 'POST' and $confirmed) {
252 | 0 |  | 0 |  |  | 0 | my $return_to = $page->parent // $page->repository->default_file;
253 | 0 | 0 |  |  |  | 0 | if ($return_to->full_path ne $page->full_path) {
254 | 0 | 0 |  |  |  | 0 | if (my $user = $ctx->session->{user}) {
255 | 0 |  |  |  |  | 0 | $page->author_name($user->{name});
256 | 0 |  |  |  |  | 0 | $page->author_email($user->{email});
257 |  |  |  |  |  |  | }
258 |
259 |  |  |  |  |  |  | $page->remove({
260 | 0 |  |  |  |  | 0 | comment => 'Removing ' . $page->full_path . ' from repository.',
261 |  |  |  |  |  |  | });
262 |
263 | 0 |  |  |  |  | 0 | $ctx->response->redirect(join '/', '/page/view', $repo_name, $return_to->full_path);
264 | 0 |  |  |  |  | 0 | return;
265 |
266 |  |  |  |  |  |  | }
267 |
268 |  |  |  |  |  |  | else {
269 | 0 |  |  |  |  | 0 | $ctx->add_errors('you may not remove the top-most page of a repository');
270 |  |  |  |  |  |  | }
271 |  |  |  |  |  |  | }
272 |
273 |  |  |  |  |  |  | $ctx->response->body(
274 | 0 |  |  |  |  | 0 | $self->view('Page')->remove($ctx, {
275 |  |  |  |  |  |  | title => $page->title,
276 |  |  |  |  |  |  | breadcrumb => $breadcrumb,
277 |  |  |  |  |  |  | repository => $repo_name,
278 |  |  |  |  |  |  | page => $page->full_path,
279 |  |  |  |  |  |  | file => $page,
280 |  |  |  |  |  |  | return_link => join('/', '/page/view', $repo_name, $page->full_path),
281 |  |  |  |  |  |  | })
282 |  |  |  |  |  |  | );
283 |  |  |  |  |  |  | }
284 |
285 - 289 |  |
    =head2 view_history

    Displays the page's revision history.

    =cut
290 |
291 |  |  |  |  |  |  | sub view_history {
292 | 0 |  |  | 0 | 1 | 0 | my ($self, $ctx) = @_;
293 |
294 | 0 |  |  |  |  | 0 | my ($repo_name, $path) = $self->repo_name_and_path($ctx);
295 |
296 | 0 |  |  |  |  | 0 | my $page = $self->lookup_page($repo_name, $path);
297 |
298 | 0 |  |  |  |  | 0 | my $breadcrumb = $self->breadcrumb($page->repository, $path);
299 |
300 | 0 |  |  |  |  | 0 | $ctx->response->body(
301 |  |  |  |  |  |  | $self->view('Page')->history($ctx, {
302 |  |  |  |  |  |  | title => $page->title,
303 |  |  |  |  |  |  | breadcrumb => $breadcrumb,
304 |  |  |  |  |  |  | repository => $repo_name,
305 |  |  |  |  |  |  | page => $page->full_path,
306 |  |  |  |  |  |  | revisions => [ $page->history ],
307 |  |  |  |  |  |  | })
308 |  |  |  |  |  |  | );
309 |  |  |  |  |  |  | }
310 |
311 - 315 |  |
    =head2 view_diff

    Displays a diff of the page.

    =cut
316 |
317 |  |  |  |  |  |  | sub view_diff {
318 | 0 |  |  | 0 | 1 | 0 | my ($self, $ctx) = @_;
319 |
320 | 0 |  |  |  |  | 0 | my ($repo_name, $path) = $self->repo_name_and_path($ctx);
321 |
322 | 0 |  |  |  |  | 0 | my $page = $self->lookup_page($repo_name, $path);
323 |
324 | 0 |  |  |  |  | 0 | my $breadcrumb = $self->breadcrumb($page->repository, $path);
325 |
326 | 0 |  |  |  |  | 0 | my $r1 = $ctx->request->query_parameters->{r1};
327 | 0 |  |  |  |  | 0 | my $r2 = $ctx->request->query_parameters->{r2};
328 |
329 |  |  |  |  |  |  | try {
330 |
331 | 0 |  |  | 0 |  | 0 | my $diff = '';
332 | 0 |  |  |  |  | 0 | for my $chunk ($page->diff($r1, $r2)) {
333 | 0 0 | 0 0 0 |  |  |  | 0 0 | if ($chunk->[0] eq ' ') { $diff .= $chunk->[1] }
334 | 0 |  |  |  |  | 0 | elsif ($chunk->[0] eq '+') { $diff .= sprintf '<ins markdown="1">%s</ins>', $chunk->[1] }
335 | 0 |  |  |  |  | 0 | elsif ($chunk->[0] eq '-') { $diff .= sprintf '<del markdown="1">%s</del>', $chunk->[1] }
336 | 0 |  |  |  |  | 0 | else { warn "unknown chunk type $chunk->[0]" }
337 |  |  |  |  |  |  | }
338 |
339 | 0 |  |  |  |  | 0 | my $file_preview = $page->file_preview(
340 |  |  |  |  |  |  | content => $diff,
341 |  |  |  |  |  |  | );
342 |
343 | 0 |  |  |  |  | 0 | $ctx->response->body(
344 |  |  |  |  |  |  | $self->view('Page')->diff($ctx, {
345 |  |  |  |  |  |  | title => $page->title,
346 |  |  |  |  |  |  | breadcrumb => $breadcrumb,
347 |  |  |  |  |  |  | repository => $repo_name,
348 |  |  |  |  |  |  | page => $page->full_path,
349 |  |  |  |  |  |  | file => $file_preview,
350 |  |  |  |  |  |  | })
351 |  |  |  |  |  |  | );
352 |  |  |  |  |  |  | }
353 |
354 |  |  |  |  |  |  | catch {
355 | 0 |  |  | 0 |  | 0 | my $ERROR = $_;
356 | 0 | 0 |  |  |  | 0 | if ("$_" =~ /usage: git diff/) {
357 | 0 |  |  |  |  | 0 | http_throw 'Diffs will not work with git versions before 1.7.2. Please use a newer version of git. If you are using a newer version of git, please file a support issue.';
358 |  |  |  |  |  |  | }
359 | 0 |  |  |  |  | 0 | die $ERROR;
360 | 0 |  |  |  |  | 0 | };
361 |  |  |  |  |  |  | }
362 |
363 - 367 |  |
    =head2 preview_page

    Shows the preview for an edit to a page using L<Yukki::Web::View::Page/preview>..

    =cut
368 |
369 |  |  |  |  |  |  | sub preview_page {
370 | 0 |  |  | 0 | 1 | 0 | my ($self, $ctx) = @_;
371 |
372 | 0 |  |  |  |  | 0 | my ($repo_name, $path) = $self->repo_name_and_path($ctx);
373 |
374 | 0 |  |  |  |  | 0 | my $page = $self->lookup_page($repo_name, $path);
375 |
376 | 0 |  |  |  |  | 0 | my $breadcrumb = $self->breadcrumb($page->repository, $path);
377 |
378 | 0 |  |  |  |  | 0 | my $content = $ctx->request->body_parameters->{yukkitext};
379 | 0 |  |  |  |  | 0 | my $position = $ctx->request->parameters->{yukkitext_position};
380 | 0 |  |  |  |  | 0 | my $file_preview = $page->file_preview(
381 |  |  |  |  |  |  | content => $content,
382 |  |  |  |  |  |  | position => $position,
383 |  |  |  |  |  |  | );
384 |
385 | 0 |  |  |  |  | 0 | $ctx->response->body(
386 |  |  |  |  |  |  | $self->view('Page')->preview($ctx, {
387 |  |  |  |  |  |  | title => $page->title,
388 |  |  |  |  |  |  | breadcrumb => $breadcrumb,
389 |  |  |  |  |  |  | repository => $repo_name,
390 |  |  |  |  |  |  | page => $page->full_path,
391 |  |  |  |  |  |  | file => $file_preview,
392 |  |  |  |  |  |  | })
393 |  |  |  |  |  |  | );
394 |  |  |  |  |  |  | }
395 |
396 - 400 |  |
    =head2 upload_attachment

    This is a facade that wraps L<Yukki::Web::Controller::Attachment/upload>.

    =cut
401 |
402 |  |  |  |  |  |  | sub upload_attachment {
403 | 0 |  |  | 0 | 1 | 0 | my ($self, $ctx) = @_;
404 |
405 | 0 |  |  |  |  | 0 | my $repo_name = $ctx->request->path_parameters->{repository};
406 | 0 |  |  |  |  | 0 | my $path = delete $ctx->request->path_parameters->{page};
407 |
408 | 0 |  |  |  |  | 0 | my $page = $self->lookup_page($repo_name, $path);
409 |
410 | 0 |  |  |  |  | 0 | my @file = split m{/}, $page->path;
411 | 0 |  |  |  |  | 0 | push @file, $ctx->request->uploads->{file}->filename;
412 |
413 | 0 |  |  |  |  | 0 | $ctx->request->path_parameters->{action} = 'upload';
414 | 0 |  |  |  |  | 0 | $ctx->request->path_parameters->{file} = \@file;
415 |
416 | 0 |  |  |  |  | 0 | $self->controller('Attachment')->fire($ctx);
417 |  |  |  |  |  |  | }
418 |
419 - 423 |  |
    =head2 breadcrumb

    Given the repository and path, returns the breadcrumb.

    =cut
424 |
425 |  |  |  |  |  |  | sub breadcrumb {
426 | 1 |  |  | 1 | 1 | 2 | my ($self, $repository, $path_parts) = @_;
427 |
428 | 1 |  |  |  |  | 2 | my @breadcrumb;
429 |  |  |  |  |  |  | my @path_acc;
430 |
431 | 1 |  |  |  |  | 12 | push @breadcrumb, {
432 |  |  |  |  |  |  | label => $repository->title,
433 |  |  |  |  |  |  | href => join('/', '/page/view/', $repository->name),
434 |  |  |  |  |  |  | };
435 |
436 | 1 |  |  |  |  | 38 | for my $path_part (@$path_parts) {
437 | 0 |  |  |  |  | 0 | push @path_acc, $path_part;
438 | 0 |  |  |  |  | 0 | my $file = $repository->file({
439 |  |  |  |  |  |  | path => join('/', @path_acc),
440 |  |  |  |  |  |  | filetype => 'yukki',
441 |  |  |  |  |  |  | });
442 |
443 | 0 |  |  |  |  | 0 | push @breadcrumb, {
444 |  |  |  |  |  |  | label => $file->title,
445 |  |  |  |  |  |  | href => join('/', '/page/view', $repository->name, $file->full_path),
446 |  |  |  |  |  |  | };
447 |  |  |  |  |  |  | }
448 |
449 | 1 |  |  |  |  | 2 | return \@breadcrumb;
450 |  |  |  |  |  |  | }
451 |
452 |  |  |  |  |  |  | 1;